dominusnode-dify 2.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.
- dominusnode_dify/__init__.py +11 -0
- dominusnode_dify/_core.py +453 -0
- dominusnode_dify/plugin.py +34 -0
- dominusnode_dify/provider.py +36 -0
- dominusnode_dify-2.1.0.dist-info/METADATA +103 -0
- dominusnode_dify-2.1.0.dist-info/RECORD +9 -0
- dominusnode_dify-2.1.0.dist-info/WHEEL +5 -0
- dominusnode_dify-2.1.0.dist-info/licenses/LICENSE +21 -0
- dominusnode_dify-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""DominusNode relay-network tool provider for Dify (v2, built on the dominusnode SDK).
|
|
2
|
+
|
|
3
|
+
``DominusNodeProvider`` works without Dify; the ``dify_plugin`` classes live in
|
|
4
|
+
``dominusnode_dify.plugin`` (imported by ``provider/`` and ``tools/`` in the plugin package).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dominusnode_dify._core import TOOL_NAMES, TOOL_SPECS, DominusNodeToolkit
|
|
8
|
+
from dominusnode_dify.provider import DominusNodeProvider
|
|
9
|
+
|
|
10
|
+
__version__ = "2.1.0"
|
|
11
|
+
__all__ = ["DominusNodeProvider", "DominusNodeToolkit", "TOOL_NAMES", "TOOL_SPECS", "__version__"]
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""DominusNode v2 standard tool set (19 tools), built on the ``dominusnode`` SDK.
|
|
2
|
+
|
|
3
|
+
Framework adapters in this package wrap :class:`DominusNodeToolkit` and
|
|
4
|
+
:data:`TOOL_SPECS`. All HTTP, SSRF/DNS-rebinding checks, proxy tunnelling,
|
|
5
|
+
validation and credential scrubbing live in the SDK; this module only maps
|
|
6
|
+
tool calls to SDK calls and makes every result safe to hand to a model.
|
|
7
|
+
|
|
8
|
+
The API key comes from ``DOMINUSNODE_API_KEY`` (or the ``api_key`` config
|
|
9
|
+
argument). It is never a tool argument and never appears in tool output.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
18
|
+
from urllib.parse import quote
|
|
19
|
+
|
|
20
|
+
from dominusnode import DominusNodeClient, json_default, scrub_credentials
|
|
21
|
+
|
|
22
|
+
TOOL_NAMES: List[str] = [
|
|
23
|
+
"fetch",
|
|
24
|
+
"proxy_url",
|
|
25
|
+
"network_policy",
|
|
26
|
+
"network_pricing",
|
|
27
|
+
"network_stats",
|
|
28
|
+
"balance",
|
|
29
|
+
"transactions",
|
|
30
|
+
"topup_crypto",
|
|
31
|
+
"check_payment",
|
|
32
|
+
"usage",
|
|
33
|
+
"active_sessions",
|
|
34
|
+
"report_session",
|
|
35
|
+
"node_list",
|
|
36
|
+
"node_tier",
|
|
37
|
+
"node_setup_guide",
|
|
38
|
+
"credits_balance",
|
|
39
|
+
"credits_convert",
|
|
40
|
+
"payouts_list",
|
|
41
|
+
"x402_info",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
DEFAULT_MAX_CHARS = 4000
|
|
45
|
+
_MAX_CHARS_CAP = 100_000
|
|
46
|
+
_KEY_PLACEHOLDER = "<DOMINUSNODE_API_KEY>"
|
|
47
|
+
_SECRET_RE = re.compile(r"dn_(?:live|test|node|proxy)_[A-Za-z0-9_-]+")
|
|
48
|
+
|
|
49
|
+
NODE_SETUP_GUIDE: Dict[str, Any] = {
|
|
50
|
+
"summary": (
|
|
51
|
+
"Earn credits by contributing an IP to the DominusNode relay network. A node agent on your "
|
|
52
|
+
"machine dials OUT to the gateway (no inbound ports) and relays other agents' GET/HEAD "
|
|
53
|
+
"traffic on ports 80/443 only. You earn your tier's share of each paid GB relayed; live "
|
|
54
|
+
"prices and shares come from network_pricing, tier progress from node_tier."
|
|
55
|
+
),
|
|
56
|
+
"rules": [
|
|
57
|
+
"Self-dealing earns nothing: your own, your wallet's, your team's or your /24's traffic is never routed to your node.",
|
|
58
|
+
"Node floor: ports 80/443 only, never private/loopback/metadata targets, never SMTP/SSH/RDP, metadata-only logging (see network_policy).",
|
|
59
|
+
"IPs in sanctioned countries (CU, IR, KP, RU, SY) cannot join.",
|
|
60
|
+
"Tiers rise with paid tenure, uptime, distinct active IPs and reputation.",
|
|
61
|
+
],
|
|
62
|
+
"prerequisites": [
|
|
63
|
+
"A wallet-linked account and a dn_live_ API key (not a team key).",
|
|
64
|
+
"Node.js >= 18 and git on the machine whose IP you contribute.",
|
|
65
|
+
],
|
|
66
|
+
"steps": [
|
|
67
|
+
"1. Store the owner wallet private key in a 0600 file, e.g. ~/.dominusnode/wallet.key (never pass it as an argument).",
|
|
68
|
+
"2. Register the node (signing happens locally, not through this tool set): either the DominusNode MCP server "
|
|
69
|
+
"(DOMINUSNODE_WALLET_KEY_FILE set, tool dominusnode_node_register) or the node agent CLI: "
|
|
70
|
+
"git clone https://github.com/0xcircuitbreaker/DomiNode.git && cd DomiNode && npm ci && "
|
|
71
|
+
"npm run build -w @dominusnode/shared -w @dominusnode/node-agent && "
|
|
72
|
+
"node packages/node-agent/dist/src/main.js register --wallet-key-file ~/.dominusnode/wallet.key "
|
|
73
|
+
"(API key from the DN_API_KEY environment variable).",
|
|
74
|
+
"3. Run the agent on the machine that owns the IP: node packages/node-agent/dist/src/main.js run "
|
|
75
|
+
"--key-file ~/.dominusnode/node.json (optional limits: --ports 443 --max-streams 32 --gb-per-day 5). "
|
|
76
|
+
"Keep it running (systemd, pm2 or a container restart policy); uptime drives your tier.",
|
|
77
|
+
"4. Verify with node_list, then watch earnings with credits_balance and convert them with credits_convert.",
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _redact_str(s: str, key: Optional[str]) -> str:
|
|
83
|
+
if key:
|
|
84
|
+
s = s.replace(key, "[REDACTED]").replace(quote(key, safe=""), "[REDACTED]")
|
|
85
|
+
return scrub_credentials(s)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _sanitize(obj: Any, key: Optional[str], depth: int = 0) -> Any:
|
|
89
|
+
"""Make a result JSON-safe and strip any credential from every string in it."""
|
|
90
|
+
if depth > 32:
|
|
91
|
+
return "[TRUNCATED]"
|
|
92
|
+
if isinstance(obj, str):
|
|
93
|
+
return _redact_str(obj, key)
|
|
94
|
+
if isinstance(obj, dict):
|
|
95
|
+
return {_redact_str(str(k), key): _sanitize(v, key, depth + 1) for k, v in obj.items()}
|
|
96
|
+
if isinstance(obj, (list, tuple)):
|
|
97
|
+
return [_sanitize(v, key, depth + 1) for v in obj]
|
|
98
|
+
if obj is None or isinstance(obj, (bool, int, float)):
|
|
99
|
+
return obj
|
|
100
|
+
try:
|
|
101
|
+
plain = json_default(obj)
|
|
102
|
+
except TypeError:
|
|
103
|
+
return _redact_str(str(obj), key)
|
|
104
|
+
if plain is obj or not isinstance(plain, (str, dict, list, tuple, bool, int, float, type(None))):
|
|
105
|
+
return _redact_str(str(plain), key)
|
|
106
|
+
return _sanitize(plain, key, depth + 1)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DominusNodeToolkit:
|
|
110
|
+
"""The 19 standard DominusNode tools as plain methods returning JSON-safe dicts.
|
|
111
|
+
|
|
112
|
+
Every method catches errors and returns ``{"error": "<scrubbed message>"}``
|
|
113
|
+
instead of raising, so a model never sees a stack trace or a credential.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
api_key: DominusNode API key. Defaults to ``DOMINUSNODE_API_KEY``.
|
|
117
|
+
base_url: API base URL. Defaults to ``DOMINUSNODE_BASE_URL`` / ``DOMINUSNODE_API_URL``
|
|
118
|
+
or the SDK default.
|
|
119
|
+
proxy_host: Gateway host. Defaults to ``DOMINUSNODE_PROXY_HOST`` or the SDK default.
|
|
120
|
+
client: A pre-built ``dominusnode.DominusNodeClient`` (advanced/testing).
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
api_key: Optional[str] = None,
|
|
126
|
+
base_url: Optional[str] = None,
|
|
127
|
+
proxy_host: Optional[str] = None,
|
|
128
|
+
client: Any = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
self.__api_key = api_key or os.environ.get("DOMINUSNODE_API_KEY") or None
|
|
131
|
+
self._base_url = base_url or os.environ.get("DOMINUSNODE_BASE_URL") or os.environ.get("DOMINUSNODE_API_URL")
|
|
132
|
+
self._proxy_host = proxy_host or os.environ.get("DOMINUSNODE_PROXY_HOST")
|
|
133
|
+
self._client = client
|
|
134
|
+
self._connected = False
|
|
135
|
+
|
|
136
|
+
def __repr__(self) -> str:
|
|
137
|
+
return f"DominusNodeToolkit(api_key={'[REDACTED]' if self.__api_key else None})"
|
|
138
|
+
|
|
139
|
+
__str__ = __repr__
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------ plumbing
|
|
142
|
+
def _sdk(self) -> Any:
|
|
143
|
+
if self._client is None:
|
|
144
|
+
kwargs: Dict[str, Any] = {}
|
|
145
|
+
if self._base_url:
|
|
146
|
+
kwargs["base_url"] = self._base_url
|
|
147
|
+
if self._proxy_host:
|
|
148
|
+
kwargs["proxy_host"] = self._proxy_host
|
|
149
|
+
self._client = DominusNodeClient(**kwargs)
|
|
150
|
+
if not self._connected:
|
|
151
|
+
if not self.__api_key:
|
|
152
|
+
raise RuntimeError("DOMINUSNODE_API_KEY is not set")
|
|
153
|
+
self._client.connect_with_key(self.__api_key)
|
|
154
|
+
self._connected = True
|
|
155
|
+
return self._client
|
|
156
|
+
|
|
157
|
+
def _run(self, fn: Callable[[Any], Any]) -> Dict[str, Any]:
|
|
158
|
+
try:
|
|
159
|
+
result = fn(self._sdk())
|
|
160
|
+
except Exception as exc: # noqa: BLE001 - every error is reported to the model, scrubbed
|
|
161
|
+
msg = str(exc) or type(exc).__name__
|
|
162
|
+
return {"error": _redact_str(msg, self.__api_key)}
|
|
163
|
+
out = _sanitize(result, self.__api_key)
|
|
164
|
+
return out if isinstance(out, dict) else {"result": out}
|
|
165
|
+
|
|
166
|
+
def close(self) -> None:
|
|
167
|
+
"""Disconnect the SDK client and drop the stored credentials."""
|
|
168
|
+
if self._client is not None:
|
|
169
|
+
try:
|
|
170
|
+
self._client.close()
|
|
171
|
+
except Exception: # noqa: BLE001
|
|
172
|
+
pass
|
|
173
|
+
self._client = None
|
|
174
|
+
self._connected = False
|
|
175
|
+
|
|
176
|
+
def call(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
177
|
+
"""Dispatch a tool by its standard name with a dict of arguments."""
|
|
178
|
+
if name not in TOOL_NAMES:
|
|
179
|
+
return {"error": f"unknown tool: {name}"}
|
|
180
|
+
args = dict(arguments or {})
|
|
181
|
+
allowed = set(TOOL_SPECS[name]["parameters"]["properties"])
|
|
182
|
+
unknown = sorted(set(args) - allowed)
|
|
183
|
+
if unknown:
|
|
184
|
+
return {"error": f"unknown argument(s): {', '.join(unknown)}"}
|
|
185
|
+
try:
|
|
186
|
+
return getattr(self, name)(**args)
|
|
187
|
+
except TypeError as exc:
|
|
188
|
+
return {"error": _redact_str(str(exc), self.__api_key)}
|
|
189
|
+
|
|
190
|
+
def call_json(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
|
|
191
|
+
return json.dumps(self.call(name, arguments), ensure_ascii=False)
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------------ 1-2 proxy
|
|
194
|
+
def fetch(
|
|
195
|
+
self,
|
|
196
|
+
url: str,
|
|
197
|
+
method: str = "GET",
|
|
198
|
+
country: Optional[str] = None,
|
|
199
|
+
ip_class: Optional[str] = None,
|
|
200
|
+
session_id: Optional[str] = None,
|
|
201
|
+
allow_http: bool = False,
|
|
202
|
+
max_chars: int = DEFAULT_MAX_CHARS,
|
|
203
|
+
) -> Dict[str, Any]:
|
|
204
|
+
def go(c: Any) -> Dict[str, Any]:
|
|
205
|
+
res = c.proxy.fetch(
|
|
206
|
+
url,
|
|
207
|
+
method=str(method).upper(),
|
|
208
|
+
country=country or None,
|
|
209
|
+
ip_class=ip_class or None,
|
|
210
|
+
session_id=session_id or None,
|
|
211
|
+
allow_http=bool(allow_http),
|
|
212
|
+
)
|
|
213
|
+
limit = max(0, min(int(max_chars or DEFAULT_MAX_CHARS), _MAX_CHARS_CAP))
|
|
214
|
+
text = res.text()
|
|
215
|
+
return {
|
|
216
|
+
"status": res.status,
|
|
217
|
+
"statusText": res.status_text,
|
|
218
|
+
"headers": dict(res.headers),
|
|
219
|
+
"body": text[:limit],
|
|
220
|
+
"bodyTruncated": bool(res.truncated) or len(text) > limit,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return self._run(go)
|
|
224
|
+
|
|
225
|
+
def proxy_url(
|
|
226
|
+
self,
|
|
227
|
+
country: Optional[str] = None,
|
|
228
|
+
ip_class: Optional[str] = None,
|
|
229
|
+
session_id: Optional[str] = None,
|
|
230
|
+
allow_http: bool = False,
|
|
231
|
+
protocol: str = "http",
|
|
232
|
+
) -> Dict[str, Any]:
|
|
233
|
+
def go(c: Any) -> Dict[str, Any]:
|
|
234
|
+
opts = dict(
|
|
235
|
+
country=country or None,
|
|
236
|
+
ip_class=ip_class or None,
|
|
237
|
+
session_id=session_id or None,
|
|
238
|
+
allow_http=bool(allow_http),
|
|
239
|
+
)
|
|
240
|
+
username = c.proxy.build_username(**opts)
|
|
241
|
+
url = c.proxy.build_url(protocol=protocol, **opts)
|
|
242
|
+
m = re.match(r"^([a-z0-9]+://[^:@/]+:)[^@]*(@.*)$", url)
|
|
243
|
+
masked = f"{m.group(1)}{_KEY_PLACEHOLDER}{m.group(2)}" if m else ""
|
|
244
|
+
return {
|
|
245
|
+
"username": username,
|
|
246
|
+
"url": masked,
|
|
247
|
+
"note": f"Replace {_KEY_PLACEHOLDER} with your API key from the DOMINUSNODE_API_KEY "
|
|
248
|
+
"environment variable. The key goes in the proxy PASSWORD, routing flags in the USERNAME.",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return self._run(go)
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------ 3-5 network
|
|
254
|
+
def network_policy(self) -> Dict[str, Any]:
|
|
255
|
+
return self._run(lambda c: c.network.policy())
|
|
256
|
+
|
|
257
|
+
def network_pricing(self) -> Dict[str, Any]:
|
|
258
|
+
return self._run(lambda c: c.network.pricing())
|
|
259
|
+
|
|
260
|
+
def network_stats(self) -> Dict[str, Any]:
|
|
261
|
+
return self._run(lambda c: c.network.stats())
|
|
262
|
+
|
|
263
|
+
# ------------------------------------------------------------------ 6-9 wallet
|
|
264
|
+
def balance(self) -> Dict[str, Any]:
|
|
265
|
+
return self._run(lambda c: c.wallet.get_balance())
|
|
266
|
+
|
|
267
|
+
def transactions(self, limit: Optional[int] = 20, offset: Optional[int] = None) -> Dict[str, Any]:
|
|
268
|
+
return self._run(lambda c: c.wallet.get_transactions(limit=limit, offset=offset))
|
|
269
|
+
|
|
270
|
+
def topup_crypto(self, amount_cents: int, currency: str) -> Dict[str, Any]:
|
|
271
|
+
return self._run(lambda c: c.wallet.top_up_crypto(amount_cents, currency))
|
|
272
|
+
|
|
273
|
+
def check_payment(self, invoice_id: str) -> Dict[str, Any]:
|
|
274
|
+
return self._run(lambda c: c.wallet.get_crypto_status(invoice_id))
|
|
275
|
+
|
|
276
|
+
# ------------------------------------------------------------------ 10-12 usage/sessions
|
|
277
|
+
def usage(self, since: Optional[str] = None, until: Optional[str] = None, limit: Optional[int] = 20) -> Dict[str, Any]:
|
|
278
|
+
return self._run(lambda c: c.usage.get(from_=since or None, to=until or None, limit=limit))
|
|
279
|
+
|
|
280
|
+
def active_sessions(self) -> Dict[str, Any]:
|
|
281
|
+
return self._run(lambda c: c.sessions.get_active())
|
|
282
|
+
|
|
283
|
+
def report_session(self, session_id: str, reason: str) -> Dict[str, Any]:
|
|
284
|
+
return self._run(lambda c: c.sessions.report(session_id, reason))
|
|
285
|
+
|
|
286
|
+
# ------------------------------------------------------------------ 13-15 nodes
|
|
287
|
+
def node_list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> Dict[str, Any]:
|
|
288
|
+
return self._run(lambda c: c.nodes.list(limit=limit, offset=offset))
|
|
289
|
+
|
|
290
|
+
def node_tier(self) -> Dict[str, Any]:
|
|
291
|
+
return self._run(lambda c: c.nodes.tier())
|
|
292
|
+
|
|
293
|
+
def node_setup_guide(self) -> Dict[str, Any]:
|
|
294
|
+
return json.loads(json.dumps(NODE_SETUP_GUIDE))
|
|
295
|
+
|
|
296
|
+
# ------------------------------------------------------------------ 16-19 credits/payouts/x402
|
|
297
|
+
def credits_balance(self) -> Dict[str, Any]:
|
|
298
|
+
return self._run(lambda c: c.credits.balance())
|
|
299
|
+
|
|
300
|
+
def credits_convert(self, amount_musd: str, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
|
301
|
+
return self._run(lambda c: c.credits.convert(str(amount_musd), idempotency_key=idempotency_key or None))
|
|
302
|
+
|
|
303
|
+
def payouts_list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> Dict[str, Any]:
|
|
304
|
+
return self._run(lambda c: c.payouts.list(limit=limit, offset=offset))
|
|
305
|
+
|
|
306
|
+
def x402_info(self) -> Dict[str, Any]:
|
|
307
|
+
return self._run(lambda c: c.x402.get_info())
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------- tool specs
|
|
311
|
+
def _obj(props: Dict[str, Any], required: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
312
|
+
return {"type": "object", "properties": props, "required": required or [], "additionalProperties": False}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
_COUNTRY = {"type": "string", "description": "ISO 3166-1 alpha-2 exit country, e.g. US, DE. Sanctioned countries (CU, IR, KP, RU, SY) are refused."}
|
|
316
|
+
_IP_CLASS = {"type": "string", "enum": ["residential", "dc", "mobile", "masked"], "description": "Exit IP class. Omit to let the relay choose. Prices differ by class: see network_pricing. masked = cheapest, VPN/anonymizer exits, opt-in."}
|
|
317
|
+
_SESSION = {"type": "string", "description": "Sticky session id (1-64 letters/digits) to keep the same exit node across requests."}
|
|
318
|
+
_ALLOW_HTTP = {"type": "boolean", "default": False, "description": "Opt in to plaintext http:// targets. Plaintext traffic is visible to the exit node; leave false for https."}
|
|
319
|
+
_LIMIT = {"type": "integer", "minimum": 1, "maximum": 200, "description": "Maximum rows to return."}
|
|
320
|
+
_OFFSET = {"type": "integer", "minimum": 0, "description": "Rows to skip (pagination)."}
|
|
321
|
+
|
|
322
|
+
TOOL_SPECS: Dict[str, Dict[str, Any]] = {
|
|
323
|
+
"fetch": {
|
|
324
|
+
"description": (
|
|
325
|
+
"Fetch a URL through the DominusNode relay network (agent-contributed exit nodes). GET or HEAD only, "
|
|
326
|
+
"ports 80/443, no redirects followed, private/internal targets refused. https by default; plaintext "
|
|
327
|
+
"http:// needs allow_http=true and is visible to the exit node. Billed per GB from your prepaid wallet "
|
|
328
|
+
"at the live rate in network_pricing. Returns status, headers and the body (truncated to max_chars)."
|
|
329
|
+
),
|
|
330
|
+
"parameters": _obj(
|
|
331
|
+
{
|
|
332
|
+
"url": {"type": "string", "description": "Target URL (https://... ; http:// only with allow_http)."},
|
|
333
|
+
"method": {"type": "string", "enum": ["GET", "HEAD"], "default": "GET", "description": "HTTP method."},
|
|
334
|
+
"country": _COUNTRY,
|
|
335
|
+
"ip_class": _IP_CLASS,
|
|
336
|
+
"session_id": _SESSION,
|
|
337
|
+
"allow_http": _ALLOW_HTTP,
|
|
338
|
+
"max_chars": {"type": "integer", "minimum": 1, "maximum": _MAX_CHARS_CAP, "default": DEFAULT_MAX_CHARS, "description": "Maximum body characters returned."},
|
|
339
|
+
},
|
|
340
|
+
["url"],
|
|
341
|
+
),
|
|
342
|
+
},
|
|
343
|
+
"proxy_url": {
|
|
344
|
+
"description": (
|
|
345
|
+
"Build a relay proxy URL for use with any HTTP or SOCKS5 client. Routing flags (country, class, sticky "
|
|
346
|
+
"session, allowhttp) go in the proxy username; the account credential is shown only as the placeholder "
|
|
347
|
+
"<DOMINUSNODE_API_KEY>, which the caller fills from its own environment."
|
|
348
|
+
),
|
|
349
|
+
"parameters": _obj(
|
|
350
|
+
{
|
|
351
|
+
"country": _COUNTRY,
|
|
352
|
+
"ip_class": _IP_CLASS,
|
|
353
|
+
"session_id": _SESSION,
|
|
354
|
+
"allow_http": _ALLOW_HTTP,
|
|
355
|
+
"protocol": {"type": "string", "enum": ["http", "socks5"], "default": "http", "description": "Proxy protocol."},
|
|
356
|
+
}
|
|
357
|
+
),
|
|
358
|
+
},
|
|
359
|
+
"network_policy": {
|
|
360
|
+
"description": "Relay network policy: allowed ports and methods, blocked targets and countries, node rules. Read before routing unusual traffic.",
|
|
361
|
+
"parameters": _obj({}),
|
|
362
|
+
},
|
|
363
|
+
"network_pricing": {
|
|
364
|
+
"description": "Live per-GB relay prices by IP class (in micro-USD) and the node operator earning shares by tier. The only source of prices: never assume a price.",
|
|
365
|
+
"parameters": _obj({}),
|
|
366
|
+
},
|
|
367
|
+
"network_stats": {
|
|
368
|
+
"description": "Relay network size: online nodes by IP class and by country. Use it to pick a country/class with capacity.",
|
|
369
|
+
"parameters": _obj({}),
|
|
370
|
+
},
|
|
371
|
+
"balance": {
|
|
372
|
+
"description": "Prepaid wallet balance (cents) that pays for relay traffic.",
|
|
373
|
+
"parameters": _obj({}),
|
|
374
|
+
},
|
|
375
|
+
"transactions": {
|
|
376
|
+
"description": "Recent prepaid wallet transactions (top-ups and traffic charges).",
|
|
377
|
+
"parameters": _obj({"limit": _LIMIT, "offset": _OFFSET}),
|
|
378
|
+
},
|
|
379
|
+
"topup_crypto": {
|
|
380
|
+
"description": "Create a crypto invoice to top up the prepaid wallet ($10-$1,000). Returns a payment address/link; poll check_payment with the invoice id.",
|
|
381
|
+
"parameters": _obj(
|
|
382
|
+
{
|
|
383
|
+
"amount_cents": {"type": "integer", "minimum": 1000, "maximum": 100000, "description": "Amount in US cents (1000 = $10)."},
|
|
384
|
+
"currency": {"type": "string", "description": "Crypto currency code, e.g. usdc, usdt, btc, eth, sol."},
|
|
385
|
+
},
|
|
386
|
+
["amount_cents", "currency"],
|
|
387
|
+
),
|
|
388
|
+
},
|
|
389
|
+
"check_payment": {
|
|
390
|
+
"description": "Status of a crypto top-up invoice created by topup_crypto.",
|
|
391
|
+
"parameters": _obj({"invoice_id": {"type": "string", "description": "Invoice UUID from topup_crypto."}}, ["invoice_id"]),
|
|
392
|
+
},
|
|
393
|
+
"usage": {
|
|
394
|
+
"description": "Relay traffic usage summary (bytes, cost, requests) with recent records for a date range.",
|
|
395
|
+
"parameters": _obj(
|
|
396
|
+
{
|
|
397
|
+
"since": {"type": "string", "description": "Start, ISO 8601 (e.g. 2026-01-01). Default: server default period."},
|
|
398
|
+
"until": {"type": "string", "description": "End, ISO 8601."},
|
|
399
|
+
"limit": _LIMIT,
|
|
400
|
+
}
|
|
401
|
+
),
|
|
402
|
+
},
|
|
403
|
+
"active_sessions": {
|
|
404
|
+
"description": "Currently active relay sessions (id, start time, status).",
|
|
405
|
+
"parameters": _obj({}),
|
|
406
|
+
},
|
|
407
|
+
"report_session": {
|
|
408
|
+
"description": "Report a bad relay session (blocked, captcha, rate_limited, slow, bad_content, other) so the exit node's reputation is adjusted and it is avoided.",
|
|
409
|
+
"parameters": _obj(
|
|
410
|
+
{
|
|
411
|
+
"session_id": {"type": "string", "description": "Session UUID (from active_sessions)."},
|
|
412
|
+
"reason": {"type": "string", "enum": ["blocked", "captcha", "rate_limited", "slow", "bad_content", "other"], "description": "What went wrong."},
|
|
413
|
+
},
|
|
414
|
+
["session_id", "reason"],
|
|
415
|
+
),
|
|
416
|
+
},
|
|
417
|
+
"node_list": {
|
|
418
|
+
"description": "Your own relay nodes (the IPs you contribute to earn credits): status, class, country, reputation, last seen.",
|
|
419
|
+
"parameters": _obj({"limit": {"type": "integer", "minimum": 1, "maximum": 500, "description": "Maximum nodes."}, "offset": _OFFSET}),
|
|
420
|
+
},
|
|
421
|
+
"node_tier": {
|
|
422
|
+
"description": "Your node operator tier, earning share and progress toward the next tier.",
|
|
423
|
+
"parameters": _obj({}),
|
|
424
|
+
},
|
|
425
|
+
"node_setup_guide": {
|
|
426
|
+
"description": "How to earn by contributing an IP: steps to register and run the DominusNode node agent, and the earning rules.",
|
|
427
|
+
"parameters": _obj({}),
|
|
428
|
+
},
|
|
429
|
+
"credits_balance": {
|
|
430
|
+
"description": "Node operator earnings in micro-USD: pending, available and frozen credits, tier and payout status.",
|
|
431
|
+
"parameters": _obj({}),
|
|
432
|
+
},
|
|
433
|
+
"credits_convert": {
|
|
434
|
+
"description": "Convert available earned credits (micro-USD) into prepaid wallet balance to pay for relay traffic. Retry with the returned idempotency key.",
|
|
435
|
+
"parameters": _obj(
|
|
436
|
+
{
|
|
437
|
+
"amount_musd": {"type": "string", "description": "Amount in micro-USD as a digit string (1000000 = $1; min 10000)."},
|
|
438
|
+
"idempotency_key": {"type": "string", "description": "Optional key to make a retry safe."},
|
|
439
|
+
},
|
|
440
|
+
["amount_musd"],
|
|
441
|
+
),
|
|
442
|
+
},
|
|
443
|
+
"payouts_list": {
|
|
444
|
+
"description": "Payouts of earned credits to your payout wallet.",
|
|
445
|
+
"parameters": _obj({"limit": _LIMIT, "offset": _OFFSET}),
|
|
446
|
+
},
|
|
447
|
+
"x402_info": {
|
|
448
|
+
"description": "x402 HTTP micropayment support: facilitators, networks, currencies and server-reported pricing.",
|
|
449
|
+
"parameters": _obj({}),
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
assert list(TOOL_SPECS) == TOOL_NAMES
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Dify plugin classes (``dify_plugin`` SDK) backed by :class:`DominusNodeProvider`.
|
|
2
|
+
|
|
3
|
+
``tools/<name>.py`` each define one ``DominusNodeTool`` subclass, as Dify's loader
|
|
4
|
+
expects exactly one ``Tool`` subclass per source file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Generator
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
from dify_plugin import Tool, ToolProvider
|
|
13
|
+
from dify_plugin.entities.tool import ToolInvokeMessage
|
|
14
|
+
from dify_plugin.errors.tool import ToolProviderCredentialValidationError
|
|
15
|
+
|
|
16
|
+
from dominusnode_dify.provider import DominusNodeProvider
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DominusNodeTool(Tool):
|
|
20
|
+
"""Base for the 19 tools; subclasses set ``TOOL`` to the standard tool name."""
|
|
21
|
+
|
|
22
|
+
TOOL: str = ""
|
|
23
|
+
|
|
24
|
+
def _invoke(self, tool_parameters: Dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]:
|
|
25
|
+
provider = DominusNodeProvider(credentials=dict(self.runtime.credentials or {}))
|
|
26
|
+
params = {k: v for k, v in (tool_parameters or {}).items() if v is not None and v != ""}
|
|
27
|
+
yield self.create_json_message(provider.invoke(self.TOOL, params))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DominusNodeToolProvider(ToolProvider):
|
|
31
|
+
def _validate_credentials(self, credentials: Dict[str, Any]) -> None:
|
|
32
|
+
result = DominusNodeProvider(credentials=credentials).invoke("balance", {})
|
|
33
|
+
if "error" in result:
|
|
34
|
+
raise ToolProviderCredentialValidationError(result["error"])
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Framework-free Dify provider: ``DominusNodeProvider(credentials={...}).invoke(tool, params)``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from dominusnode_dify._core import TOOL_NAMES, DominusNodeToolkit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DominusNodeProvider:
|
|
12
|
+
"""Dispatch the 19 standard tools with Dify-style dict parameters.
|
|
13
|
+
|
|
14
|
+
``credentials`` keys: ``api_key`` (falls back to ``DOMINUSNODE_API_KEY``),
|
|
15
|
+
optional ``base_url`` and ``proxy_host``. Results are JSON-safe dicts; failures are
|
|
16
|
+
``{"error": "<scrubbed>"}``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, credentials: Optional[Dict[str, Any]] = None, client: Any = None) -> None:
|
|
20
|
+
creds = credentials or {}
|
|
21
|
+
self._toolkit = DominusNodeToolkit(
|
|
22
|
+
api_key=creds.get("api_key") or os.environ.get("DOMINUSNODE_API_KEY"),
|
|
23
|
+
base_url=creds.get("base_url") or None,
|
|
24
|
+
proxy_host=creds.get("proxy_host") or None,
|
|
25
|
+
client=client,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return "DominusNodeProvider()"
|
|
30
|
+
|
|
31
|
+
def invoke(self, tool_name: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
32
|
+
return self._toolkit.call(tool_name, params or {})
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def get_tools() -> List[str]:
|
|
36
|
+
return list(TOOL_NAMES)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dominusnode-dify
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: DominusNode agent relay network tools for Dify (19 tools, built on the dominusnode SDK)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/integrations/dify
|
|
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/dify/CHANGELOG.md
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: dominusnode<3,>=2.1.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pyyaml>=6.0; extra == "dev"
|
|
17
|
+
Requires-Dist: dify_plugin>=0.4.0; extra == "dev"
|
|
18
|
+
Provides-Extra: plugin
|
|
19
|
+
Requires-Dist: dify_plugin>=0.4.0; extra == "plugin"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# dominusnode-dify
|
|
23
|
+
|
|
24
|
+
DominusNode tools for [Dify](#usage): 19 tools for the DominusNode **agent relay network**.
|
|
25
|
+
Agents fetch the web through exit nodes contributed by other agents, pay per GB from a prepaid
|
|
26
|
+
wallet at the live price from `network_pricing`, and can earn credits by contributing their own IP
|
|
27
|
+
with the node agent (`node_setup_guide`).
|
|
28
|
+
|
|
29
|
+
Version 2 is built on the official Python SDK [`dominusnode`](../../sdks/python) (v2). All HTTP,
|
|
30
|
+
SSRF and DNS-rebinding checks, proxy tunnelling and credential scrubbing come from the SDK.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install dominusnode-dify
|
|
36
|
+
# from source, with the local SDK
|
|
37
|
+
pip install -e ../../sdks/python -e ".[dev]"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Defaults: API `https://api.dominusnode.com`, relay gateway `proxy.dominusnode.com`. Override with `DOMINUSNODE_BASE_URL` / `DOMINUSNODE_PROXY_HOST`.
|
|
41
|
+
|
|
42
|
+
Set your key in the environment (it is never a tool argument and never appears in tool output):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export DOMINUSNODE_API_KEY=dn_live_...
|
|
46
|
+
# optional: DOMINUSNODE_BASE_URL, DOMINUSNODE_PROXY_HOST
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# As a Dify plugin: package this directory (manifest.yaml, provider/, tools/, main.py) with
|
|
53
|
+
# `dify plugin package`, install it, and enter your API key as the provider credential.
|
|
54
|
+
|
|
55
|
+
# Without Dify:
|
|
56
|
+
from dominusnode_dify import DominusNodeProvider
|
|
57
|
+
|
|
58
|
+
provider = DominusNodeProvider(credentials={"api_key": "..."}) # or DOMINUSNODE_API_KEY
|
|
59
|
+
provider.invoke("fetch", {"url": "https://example.com", "country": "US"})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Tools are exposed as Dify plugin tools (`tools/<tool>.yaml` + `tools/<tool>.py`) plus `DominusNodeProvider(credentials={...}).invoke(tool, params)`.
|
|
63
|
+
|
|
64
|
+
## The 19 tools
|
|
65
|
+
|
|
66
|
+
| # | Tool | SDK call | What it does |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| 1 | `fetch` | `proxy.fetch` | GET/HEAD a URL through the relay (country, ip_class, session_id, allow_http) |
|
|
69
|
+
| 2 | `proxy_url` | `proxy.build_url` | Proxy URL/username for any client; key shown only as `<DOMINUSNODE_API_KEY>` |
|
|
70
|
+
| 3 | `network_policy` | `network.policy` | Allowed ports/methods, blocked targets and countries |
|
|
71
|
+
| 4 | `network_pricing` | `network.pricing` | Live per-GB prices by IP class and node earning shares |
|
|
72
|
+
| 5 | `network_stats` | `network.stats` | Online nodes by class and country |
|
|
73
|
+
| 6 | `balance` | `wallet.get_balance` | Prepaid wallet balance |
|
|
74
|
+
| 7 | `transactions` | `wallet.get_transactions` | Prepaid wallet transactions |
|
|
75
|
+
| 8 | `topup_crypto` | `wallet.top_up_crypto` | Crypto top-up invoice ($10-$1,000) |
|
|
76
|
+
| 9 | `check_payment` | `wallet.get_crypto_status` | Crypto invoice status |
|
|
77
|
+
| 10 | `usage` | `usage.get` | Traffic usage summary and records |
|
|
78
|
+
| 11 | `active_sessions` | `sessions.get_active` | Active relay sessions |
|
|
79
|
+
| 12 | `report_session` | `sessions.report` | Report a bad exit session |
|
|
80
|
+
| 13 | `node_list` | `nodes.list` | Your contributed nodes |
|
|
81
|
+
| 14 | `node_tier` | `nodes.tier` | Node operator tier and progress |
|
|
82
|
+
| 15 | `node_setup_guide` | `(static)` | How to earn by running the node agent |
|
|
83
|
+
| 16 | `credits_balance` | `credits.balance` | Earned credits (µUSD) |
|
|
84
|
+
| 17 | `credits_convert` | `credits.convert` | Convert credits into wallet balance |
|
|
85
|
+
| 18 | `payouts_list` | `payouts.list` | Payouts of earned credits |
|
|
86
|
+
| 19 | `x402_info` | `x402.get_info` | x402 micropayment info |
|
|
87
|
+
|
|
88
|
+
## Security
|
|
89
|
+
|
|
90
|
+
- `fetch` is GET/HEAD only, ports 80/443, no redirects, 10 MB cap, private and internal targets
|
|
91
|
+
refused after DNS resolution (all addresses). `https` by default; plaintext `http://` needs
|
|
92
|
+
`allow_http=true` and is visible to the exit node.
|
|
93
|
+
- Sanctioned countries (CU, IR, KP, RU, SY) are refused.
|
|
94
|
+
- Every error and result is scrubbed of `dn_live_`/`dn_test_`/`dn_node_`/`dn_proxy_` keys and JWTs.
|
|
95
|
+
- Signing flows (node registration, withdrawals, payout wallet) are not exposed as LLM tools; use the
|
|
96
|
+
DominusNode MCP server or the SDK with a signer key file.
|
|
97
|
+
|
|
98
|
+
## Tests
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install -e ../../sdks/python -e ".[dev]"
|
|
102
|
+
pytest
|
|
103
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
dominusnode_dify/__init__.py,sha256=hqsOXw192skFp--U189yXoSBfH1Funvs0-TKfycQKEg,526
|
|
2
|
+
dominusnode_dify/_core.py,sha256=MTYQ0D77fILpB0H-dOS2zwTQg6VUxuvvFTGO6Y-JKGQ,21168
|
|
3
|
+
dominusnode_dify/plugin.py,sha256=Q7sViiQWIyPDNR_fSlJIHKSMGpVlqiOVJZnfn0A8Yf0,1370
|
|
4
|
+
dominusnode_dify/provider.py,sha256=1FkX2JiYzxuouBxPFYYbtNC2ONZnTyFplFmFsOcGpYg,1287
|
|
5
|
+
dominusnode_dify-2.1.0.dist-info/licenses/LICENSE,sha256=NBkjfgN0tpbJjWf7uqsPCOMgq7uuSEW4uaGDCpB8HbI,1069
|
|
6
|
+
dominusnode_dify-2.1.0.dist-info/METADATA,sha256=0CFkZGIEFQyMZGpBoylqsQeqFxt9L0z05-cERnzwNTI,4787
|
|
7
|
+
dominusnode_dify-2.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
8
|
+
dominusnode_dify-2.1.0.dist-info/top_level.txt,sha256=4oa3__-1Ex0jTd_aTboMFsFq_zfJoB06bKNVz34qX1o,17
|
|
9
|
+
dominusnode_dify-2.1.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_dify
|