dominusnode-ironclaw 2.2.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,48 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0 (2026-11-06)
4
+
5
+ ### Breaking
6
+ - Rebuilt on the DominusNode Python SDK v2 (`dominusnode>=2.0.0`); the integration no longer
7
+ contains its own HTTP client, SSRF checks or proxy code.
8
+ - Exposes exactly the 19 standard v2 tools (fetch, proxy_url, network_policy, network_pricing, network_stats, balance, transactions, topup_crypto, check_payment, usage, active_sessions, report_session, node_list, node_tier, node_setup_guide, credits_balance, credits_convert, payouts_list, x402_info).
9
+ - Removed: Stripe/PayPal top-ups, plans, slots, email/password and MFA account tools, API key and
10
+ team management tools, agentic wallet tools, hardcoded prices and `dc-`/`residential-` prefixes.
11
+ - The API key comes only from `DOMINUSNODE_API_KEY` or configuration.
12
+
13
+ ## 1.0.1
14
+
15
+ - Fix branding: replace "DomiNode" with "Dominus Node" throughout
16
+
17
+ ## 1.0.0 (2026-02-24)
18
+
19
+ ### Added
20
+
21
+ - Initial release with 23 IronClaw-compatible tools for Dominus Node proxy service
22
+ - Proxied fetch with SSRF protection, OFAC blocking, and credential scrubbing
23
+ - Wallet balance and usage monitoring tools
24
+ - Proxy configuration and active session listing
25
+ - Agentic wallet management: create, fund, balance, list, transactions, freeze, unfreeze, delete
26
+ - Team management: create, list, details, fund, create key, usage, update, update member role
27
+ - x402 micropayment protocol information
28
+ - PayPal top-up tool
29
+ - IronClaw tool definitions via get_tool_definitions() with JSON Schema parameters
30
+ - MCP server configuration via get_mcp_config() for IronClaw's native MCP bridge
31
+ - Full security suite: DNS rebinding protection, Teredo/6to4 IPv6 tunnel detection, IPv4-mapped/compatible IPv6 handling, hex/octal/decimal IP normalization, prototype pollution prevention
32
+ - 120+ tests covering SSRF, sanitization, OFAC, HTTP methods, validation, tool definitions, MCP config, and mocked API calls
33
+
34
+ ## [2.2.0] - 2026-11-13
35
+
36
+ ### Changed
37
+ - Proxy traffic now goes to the gateway's TLS (HTTPS proxy) listener on port 8443 by default, and `proxy_url` returns `https://...:8443`. Production gateways refuse credentials on plaintext 8080/1080, so `fetch` failed on first use before. `DOMINUSNODE_PROXY_URL=https://host[:port]` sets the gateway host and TLS port (same variable as the MCP server; self-hosted coordinators publish 8444), `DOMINUSNODE_PROXY_TLS_PORT` sets only the port, and `DOMINUSNODE_PROXY_INSECURE=1` selects plaintext for local development.
38
+ - The node setup guide uses the published node image (`ghcr.io/0xcircuitbreaker/dominusnode-node`), which has the official coordinator pins baked in. A plain source build has none and now refuses to start.
39
+ - Requires the DominusNode SDK 2.2.0 or later (relay error codes on refused CONNECTs).
40
+
41
+ ## [2.1.0] - 2026-11-12
42
+
43
+ ### Added
44
+ - The fetch tool's `ip_class` accepts `masked` (cheapest, VPN/anonymizer exits, opt-in; never used unless asked).
45
+
46
+ ### Changed
47
+ - Requires the DominusNode SDK 2.1.0 or later (`masked` support).
48
+
@@ -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,8 @@
1
+ # sdist contents: package source, metadata and docs only (see scripts/release/check-artifacts.mjs)
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include LICENSE
5
+ prune tests
6
+ exclude conftest.py
7
+ prune examples
8
+ global-exclude *.py[cod] __pycache__ *.map .env .env.* *.log
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: dominusnode-ironclaw
3
+ Version: 2.2.0
4
+ Summary: DominusNode agent relay network tools for IronClaw (19 tools, built on the dominusnode SDK)
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/integrations/ironclaw
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/ironclaw/CHANGELOG.md
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: dominusnode<3,>=2.2.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # dominusnode-ironclaw
19
+
20
+ DominusNode tools for [IronClaw](#usage): 19 tools for the DominusNode **agent relay network**.
21
+ Agents fetch the web through exit nodes contributed by other agents, pay per GB from a prepaid
22
+ wallet at the live price from `network_pricing`, and can earn credits by contributing their own IP
23
+ with the node agent (`node_setup_guide`).
24
+
25
+ Version 2 is built on the official Python SDK [`dominusnode`](../../sdks/python) (v2). All HTTP,
26
+ SSRF and DNS-rebinding checks, proxy tunnelling and credential scrubbing come from the SDK.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install dominusnode-ironclaw
32
+ # from source, with the local SDK
33
+ pip install -e ../../sdks/python -e ".[dev]"
34
+ ```
35
+
36
+ Defaults: API `https://api.dominusnode.com`, relay gateway `proxy.dominusnode.com`. Override with `DOMINUSNODE_BASE_URL` / `DOMINUSNODE_PROXY_HOST`. Proxy traffic uses TLS to the gateway (HTTPS proxy, port 8443). For a self-hosted coordinator set `DOMINUSNODE_PROXY_URL=https://<proxy-host>:8443`; `DOMINUSNODE_PROXY_INSECURE=1` selects plaintext 8080 for local development only (production gateways refuse it).
37
+
38
+ Set your key in the environment (it is never a tool argument and never appears in tool output):
39
+
40
+ ```bash
41
+ export DOMINUSNODE_API_KEY=dn_live_...
42
+ # optional: DOMINUSNODE_BASE_URL, DOMINUSNODE_PROXY_HOST
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from dominusnode_ironclaw import DominusNodeToolkit, generate_mcp_config
49
+
50
+ toolkit = DominusNodeToolkit() # key from DOMINUSNODE_API_KEY
51
+ tools = toolkit.get_tools() # register with the IronClaw Python bridge
52
+ print(toolkit.execute("dominusnode_network_pricing"))
53
+
54
+ # Or use IronClaw's MCP bridge with the DominusNode MCP server (stdio, key by env reference only):
55
+ config = generate_mcp_config()
56
+ ```
57
+
58
+ Tools are exposed as `dominusnode_<tool>` bridge tool dicts (`name`, `description`, `parameters`, `handler`).
59
+
60
+ ## The 19 tools
61
+
62
+ | # | Tool | SDK call | What it does |
63
+ |---|---|---|---|
64
+ | 1 | `fetch` | `proxy.fetch` | GET/HEAD a URL through the relay (country, ip_class, session_id, allow_http) |
65
+ | 2 | `proxy_url` | `proxy.build_url` | Proxy URL/username for any client; key shown only as `<DOMINUSNODE_API_KEY>` |
66
+ | 3 | `network_policy` | `network.policy` | Allowed ports/methods, blocked targets and countries |
67
+ | 4 | `network_pricing` | `network.pricing` | Live per-GB prices by IP class and node earning shares |
68
+ | 5 | `network_stats` | `network.stats` | Online nodes by class and country |
69
+ | 6 | `balance` | `wallet.get_balance` | Prepaid wallet balance |
70
+ | 7 | `transactions` | `wallet.get_transactions` | Prepaid wallet transactions |
71
+ | 8 | `topup_crypto` | `wallet.top_up_crypto` | Crypto top-up invoice ($10-$1,000) |
72
+ | 9 | `check_payment` | `wallet.get_crypto_status` | Crypto invoice status |
73
+ | 10 | `usage` | `usage.get` | Traffic usage summary and records |
74
+ | 11 | `active_sessions` | `sessions.get_active` | Active relay sessions |
75
+ | 12 | `report_session` | `sessions.report` | Report a bad exit session |
76
+ | 13 | `node_list` | `nodes.list` | Your contributed nodes |
77
+ | 14 | `node_tier` | `nodes.tier` | Node operator tier and progress |
78
+ | 15 | `node_setup_guide` | `(static)` | How to earn by running the node agent |
79
+ | 16 | `credits_balance` | `credits.balance` | Earned credits (µUSD) |
80
+ | 17 | `credits_convert` | none (signed flow) | Points to the MCP server or SDK (needs a wallet signature) |
81
+ | 18 | `payouts_list` | `payouts.list` | Payouts of earned credits |
82
+ | 19 | `x402_info` | `x402.get_info` | x402 micropayment info |
83
+
84
+ ## Security
85
+
86
+ - `fetch` is GET/HEAD only, ports 80/443, no redirects, 10 MB cap, private and internal targets
87
+ refused after DNS resolution (all addresses). `https` by default; plaintext `http://` needs
88
+ `allow_http=true` and is visible to the exit node.
89
+ - Sanctioned countries (CU, IR, KP, RU, SY) are refused.
90
+ - Every error and result is scrubbed of `dn_live_`/`dn_test_`/`dn_node_`/`dn_proxy_` keys and JWTs.
91
+ - Signing flows (node registration, withdrawals, payout wallet) are not exposed as LLM tools; use the
92
+ DominusNode MCP server or the SDK with a signer key file.
93
+
94
+ ## Tests
95
+
96
+ ```bash
97
+ pip install -e ../../sdks/python -e ".[dev]"
98
+ pytest
99
+ ```
@@ -0,0 +1,82 @@
1
+ # dominusnode-ironclaw
2
+
3
+ DominusNode tools for [IronClaw](#usage): 19 tools for the DominusNode **agent relay network**.
4
+ Agents fetch the web through exit nodes contributed by other agents, pay per GB from a prepaid
5
+ wallet at the live price from `network_pricing`, and can earn credits by contributing their own IP
6
+ with the node agent (`node_setup_guide`).
7
+
8
+ Version 2 is built on the official Python SDK [`dominusnode`](../../sdks/python) (v2). All HTTP,
9
+ SSRF and DNS-rebinding checks, proxy tunnelling and credential scrubbing come from the SDK.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install dominusnode-ironclaw
15
+ # from source, with the local SDK
16
+ pip install -e ../../sdks/python -e ".[dev]"
17
+ ```
18
+
19
+ Defaults: API `https://api.dominusnode.com`, relay gateway `proxy.dominusnode.com`. Override with `DOMINUSNODE_BASE_URL` / `DOMINUSNODE_PROXY_HOST`. Proxy traffic uses TLS to the gateway (HTTPS proxy, port 8443). For a self-hosted coordinator set `DOMINUSNODE_PROXY_URL=https://<proxy-host>:8443`; `DOMINUSNODE_PROXY_INSECURE=1` selects plaintext 8080 for local development only (production gateways refuse it).
20
+
21
+ Set your key in the environment (it is never a tool argument and never appears in tool output):
22
+
23
+ ```bash
24
+ export DOMINUSNODE_API_KEY=dn_live_...
25
+ # optional: DOMINUSNODE_BASE_URL, DOMINUSNODE_PROXY_HOST
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from dominusnode_ironclaw import DominusNodeToolkit, generate_mcp_config
32
+
33
+ toolkit = DominusNodeToolkit() # key from DOMINUSNODE_API_KEY
34
+ tools = toolkit.get_tools() # register with the IronClaw Python bridge
35
+ print(toolkit.execute("dominusnode_network_pricing"))
36
+
37
+ # Or use IronClaw's MCP bridge with the DominusNode MCP server (stdio, key by env reference only):
38
+ config = generate_mcp_config()
39
+ ```
40
+
41
+ Tools are exposed as `dominusnode_<tool>` bridge tool dicts (`name`, `description`, `parameters`, `handler`).
42
+
43
+ ## The 19 tools
44
+
45
+ | # | Tool | SDK call | What it does |
46
+ |---|---|---|---|
47
+ | 1 | `fetch` | `proxy.fetch` | GET/HEAD a URL through the relay (country, ip_class, session_id, allow_http) |
48
+ | 2 | `proxy_url` | `proxy.build_url` | Proxy URL/username for any client; key shown only as `<DOMINUSNODE_API_KEY>` |
49
+ | 3 | `network_policy` | `network.policy` | Allowed ports/methods, blocked targets and countries |
50
+ | 4 | `network_pricing` | `network.pricing` | Live per-GB prices by IP class and node earning shares |
51
+ | 5 | `network_stats` | `network.stats` | Online nodes by class and country |
52
+ | 6 | `balance` | `wallet.get_balance` | Prepaid wallet balance |
53
+ | 7 | `transactions` | `wallet.get_transactions` | Prepaid wallet transactions |
54
+ | 8 | `topup_crypto` | `wallet.top_up_crypto` | Crypto top-up invoice ($10-$1,000) |
55
+ | 9 | `check_payment` | `wallet.get_crypto_status` | Crypto invoice status |
56
+ | 10 | `usage` | `usage.get` | Traffic usage summary and records |
57
+ | 11 | `active_sessions` | `sessions.get_active` | Active relay sessions |
58
+ | 12 | `report_session` | `sessions.report` | Report a bad exit session |
59
+ | 13 | `node_list` | `nodes.list` | Your contributed nodes |
60
+ | 14 | `node_tier` | `nodes.tier` | Node operator tier and progress |
61
+ | 15 | `node_setup_guide` | `(static)` | How to earn by running the node agent |
62
+ | 16 | `credits_balance` | `credits.balance` | Earned credits (µUSD) |
63
+ | 17 | `credits_convert` | none (signed flow) | Points to the MCP server or SDK (needs a wallet signature) |
64
+ | 18 | `payouts_list` | `payouts.list` | Payouts of earned credits |
65
+ | 19 | `x402_info` | `x402.get_info` | x402 micropayment info |
66
+
67
+ ## Security
68
+
69
+ - `fetch` is GET/HEAD only, ports 80/443, no redirects, 10 MB cap, private and internal targets
70
+ refused after DNS resolution (all addresses). `https` by default; plaintext `http://` needs
71
+ `allow_http=true` and is visible to the exit node.
72
+ - Sanctioned countries (CU, IR, KP, RU, SY) are refused.
73
+ - Every error and result is scrubbed of `dn_live_`/`dn_test_`/`dn_node_`/`dn_proxy_` keys and JWTs.
74
+ - Signing flows (node registration, withdrawals, payout wallet) are not exposed as LLM tools; use the
75
+ DominusNode MCP server or the SDK with a signer key file.
76
+
77
+ ## Tests
78
+
79
+ ```bash
80
+ pip install -e ../../sdks/python -e ".[dev]"
81
+ pytest
82
+ ```
@@ -0,0 +1,8 @@
1
+ """DominusNode relay-network tools for IronClaw (v2, built on the dominusnode SDK)."""
2
+
3
+ from dominusnode_ironclaw._core import TOOL_NAMES, TOOL_SPECS
4
+ from dominusnode_ironclaw.mcp_config import generate_mcp_config
5
+ from dominusnode_ironclaw.tools import TOOL_PREFIX, DominusNodeToolkit
6
+
7
+ __version__ = "2.2.0"
8
+ __all__ = ["DominusNodeToolkit", "generate_mcp_config", "TOOL_NAMES", "TOOL_SPECS", "TOOL_PREFIX", "__version__"]
@@ -0,0 +1,525 @@
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
+ "Docker 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 published node image, which has the "
70
+ "official coordinator pins baked in: copy the wallet key to /etc/dominusnode/wallet.key (owner uid 1001, mode "
71
+ "0600), then docker run --rm -e DN_API_KEY -e DN_WALLET_KEY_FILE=/run/dominusnode/wallet.key "
72
+ "-v /etc/dominusnode:/run/dominusnode ghcr.io/0xcircuitbreaker/dominusnode-node register "
73
+ "0600), then sudo rm /etc/dominusnode/wallet.key # not needed after registration "
74
+ "(API key from the DN_API_KEY environment variable).",
75
+ "3. Run the agent on the machine that owns the IP: docker run -d --name dominusnode-node --restart "
76
+ "unless-stopped -e DN_NODE_GB_PER_DAY=5 -v /etc/dominusnode/node.json:/run/dominusnode/node.json:ro "
77
+ "ghcr.io/0xcircuitbreaker/dominusnode-node. A plain source build has no coordinator pins and refuses to start; "
78
+ "for another coordinator pass --coordinator <domain> --trust sha256:<fingerprint> or DN_NODE_COORD_PINS. "
79
+ "Keep it running; uptime drives your tier.",
80
+ "4. Verify with node_list, then watch earnings with credits_balance and convert them through the MCP server or SDK with a signer key file (credits_convert explains how).",
81
+ ],
82
+ }
83
+
84
+
85
+ #: Gateway TLS (HTTPS proxy) port: production 8443; self-hosted coordinators use 8443 too.
86
+ DEFAULT_TLS_PROXY_PORT = 8443
87
+
88
+
89
+ #: credits_convert needs a wallet signature; signing flows are not exposed as LLM tools.
90
+ CREDITS_CONVERT_SIGNING_MESSAGE = (
91
+ "Converting credits needs a fresh signature from the wallet linked to this account, so this tool does not convert. Use the DominusNode MCP server (tool dominusnode_credits_convert, with DOMINUSNODE_WALLET_KEY_FILE pointing at a chmod-600 wallet key file) or a DominusNode SDK's credits.convert with a signer key file. Check available credit with credits_balance."
92
+ )
93
+
94
+
95
+ def resolve_gateway(
96
+ proxy_host: Optional[str] = None,
97
+ tls_port: Optional[int] = None,
98
+ http_port: Optional[int] = None,
99
+ insecure: Optional[bool] = None,
100
+ env: Optional[Any] = None,
101
+ ) -> Dict[str, Any]:
102
+ """Where proxy traffic goes. TLS to the gateway's HTTPS proxy is the default: production
103
+ gateways refuse credentials on plaintext 8080/1080. ``DOMINUSNODE_PROXY_URL=https://host[:port]``
104
+ sets host and TLS port (same variable as the MCP server), ``DOMINUSNODE_PROXY_TLS_PORT`` the TLS
105
+ port, and ``DOMINUSNODE_PROXY_INSECURE=1`` selects plaintext (local development only).
106
+
107
+ Returns ``{"proxy_host", "http_port", "gateway_tls"}`` where ``gateway_tls`` is ``False`` or
108
+ ``{"port": n}``, ready for ``DominusNodeClient(gateway_tls=...)``.
109
+ """
110
+ from urllib.parse import urlsplit
111
+
112
+ e = os.environ if env is None else env
113
+
114
+ def _port(v: Any) -> Optional[int]:
115
+ try:
116
+ n = int(v)
117
+ except (TypeError, ValueError):
118
+ return None
119
+ return n if 1 <= n <= 65535 else None
120
+
121
+ url_host: Optional[str] = None
122
+ url_port: Optional[int] = None
123
+ raw = (e.get("DOMINUSNODE_PROXY_URL") or "").strip()
124
+ if raw:
125
+ u = urlsplit(raw)
126
+ if u.scheme != "https" or not u.hostname:
127
+ raise ValueError("DOMINUSNODE_PROXY_URL must be https://host[:port] (TLS to the gateway)")
128
+ if u.username or u.password:
129
+ raise ValueError("DOMINUSNODE_PROXY_URL must not contain credentials")
130
+ url_host = u.hostname.lower()
131
+ try:
132
+ url_port = _port(u.port) if u.port else DEFAULT_TLS_PROXY_PORT
133
+ except ValueError:
134
+ raise ValueError("DOMINUSNODE_PROXY_URL has an invalid port") from None
135
+ if insecure is None:
136
+ insecure = (e.get("DOMINUSNODE_PROXY_INSECURE") or "").strip() == "1"
137
+ if insecure and raw:
138
+ raise ValueError("DOMINUSNODE_PROXY_URL (TLS) and DOMINUSNODE_PROXY_INSECURE=1 are mutually exclusive")
139
+ tls = _port(tls_port) or url_port or _port(e.get("DOMINUSNODE_PROXY_TLS_PORT")) or DEFAULT_TLS_PROXY_PORT
140
+ return {
141
+ "proxy_host": proxy_host or url_host or (e.get("DOMINUSNODE_PROXY_HOST") or "").strip() or None,
142
+ "http_port": _port(http_port) or _port(e.get("DOMINUSNODE_PROXY_PORT")),
143
+ "gateway_tls": False if insecure else {"port": tls},
144
+ }
145
+
146
+
147
+ def _redact_str(s: str, key: Optional[str]) -> str:
148
+ if key:
149
+ s = s.replace(key, "[REDACTED]").replace(quote(key, safe=""), "[REDACTED]")
150
+ return scrub_credentials(s)
151
+
152
+
153
+ def _sanitize(obj: Any, key: Optional[str], depth: int = 0) -> Any:
154
+ """Make a result JSON-safe and strip any credential from every string in it."""
155
+ if depth > 32:
156
+ return "[TRUNCATED]"
157
+ if isinstance(obj, str):
158
+ return _redact_str(obj, key)
159
+ if isinstance(obj, dict):
160
+ return {_redact_str(str(k), key): _sanitize(v, key, depth + 1) for k, v in obj.items()}
161
+ if isinstance(obj, (list, tuple)):
162
+ return [_sanitize(v, key, depth + 1) for v in obj]
163
+ if obj is None or isinstance(obj, (bool, int, float)):
164
+ return obj
165
+ try:
166
+ plain = json_default(obj)
167
+ except TypeError:
168
+ return _redact_str(str(obj), key)
169
+ if plain is obj or not isinstance(plain, (str, dict, list, tuple, bool, int, float, type(None))):
170
+ return _redact_str(str(plain), key)
171
+ return _sanitize(plain, key, depth + 1)
172
+
173
+
174
+ class DominusNodeToolkit:
175
+ """The 19 standard DominusNode tools as plain methods returning JSON-safe dicts.
176
+
177
+ Every method catches errors and returns ``{"error": "<scrubbed message>"}``
178
+ instead of raising, so a model never sees a stack trace or a credential.
179
+
180
+ Args:
181
+ api_key: DominusNode API key. Defaults to ``DOMINUSNODE_API_KEY``.
182
+ base_url: API base URL. Defaults to ``DOMINUSNODE_BASE_URL`` / ``DOMINUSNODE_API_URL``
183
+ or the SDK default.
184
+ proxy_host: Gateway host. Defaults to ``DOMINUSNODE_PROXY_HOST`` or the SDK default.
185
+ client: A pre-built ``dominusnode.DominusNodeClient`` (advanced/testing).
186
+ """
187
+
188
+ def __init__(
189
+ self,
190
+ api_key: Optional[str] = None,
191
+ base_url: Optional[str] = None,
192
+ proxy_host: Optional[str] = None,
193
+ client: Any = None,
194
+ tls_proxy_port: Optional[int] = None,
195
+ insecure_proxy: Optional[bool] = None,
196
+ ) -> None:
197
+ self.__api_key = api_key or os.environ.get("DOMINUSNODE_API_KEY") or None
198
+ self._base_url = base_url or os.environ.get("DOMINUSNODE_BASE_URL") or os.environ.get("DOMINUSNODE_API_URL")
199
+ self._gateway = resolve_gateway(proxy_host, tls_proxy_port, None, insecure_proxy)
200
+ self._proxy_host = self._gateway["proxy_host"]
201
+ self._client = client
202
+ self._connected = False
203
+
204
+ def __repr__(self) -> str:
205
+ return f"DominusNodeToolkit(api_key={'[REDACTED]' if self.__api_key else None})"
206
+
207
+ __str__ = __repr__
208
+
209
+ # ------------------------------------------------------------------ plumbing
210
+ def _sdk(self) -> Any:
211
+ if self._client is None:
212
+ kwargs: Dict[str, Any] = {}
213
+ if self._base_url:
214
+ kwargs["base_url"] = self._base_url
215
+ if self._proxy_host:
216
+ kwargs["proxy_host"] = self._proxy_host
217
+ if self._gateway["http_port"]:
218
+ kwargs["http_proxy_port"] = self._gateway["http_port"]
219
+ kwargs["gateway_tls"] = self._gateway["gateway_tls"]
220
+ self._client = DominusNodeClient(**kwargs)
221
+ if not self._connected:
222
+ if not self.__api_key:
223
+ raise RuntimeError("DOMINUSNODE_API_KEY is not set")
224
+ self._client.connect_with_key(self.__api_key)
225
+ self._connected = True
226
+ return self._client
227
+
228
+ def _run(self, fn: Callable[[Any], Any]) -> Dict[str, Any]:
229
+ try:
230
+ result = fn(self._sdk())
231
+ except Exception as exc: # noqa: BLE001 - every error is reported to the model, scrubbed
232
+ msg = str(exc) or type(exc).__name__
233
+ return {"error": _redact_str(msg, self.__api_key)}
234
+ out = _sanitize(result, self.__api_key)
235
+ return out if isinstance(out, dict) else {"result": out}
236
+
237
+ def close(self) -> None:
238
+ """Disconnect the SDK client and drop the stored credentials."""
239
+ if self._client is not None:
240
+ try:
241
+ self._client.close()
242
+ except Exception: # noqa: BLE001
243
+ pass
244
+ self._client = None
245
+ self._connected = False
246
+
247
+ def call(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
248
+ """Dispatch a tool by its standard name with a dict of arguments."""
249
+ if name not in TOOL_NAMES:
250
+ return {"error": f"unknown tool: {name}"}
251
+ args = dict(arguments or {})
252
+ allowed = set(TOOL_SPECS[name]["parameters"]["properties"])
253
+ unknown = sorted(set(args) - allowed)
254
+ if unknown:
255
+ return {"error": f"unknown argument(s): {', '.join(unknown)}"}
256
+ try:
257
+ return getattr(self, name)(**args)
258
+ except TypeError as exc:
259
+ return {"error": _redact_str(str(exc), self.__api_key)}
260
+
261
+ def call_json(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
262
+ return json.dumps(self.call(name, arguments), ensure_ascii=False)
263
+
264
+ # ------------------------------------------------------------------ 1-2 proxy
265
+ def fetch(
266
+ self,
267
+ url: str,
268
+ method: str = "GET",
269
+ country: Optional[str] = None,
270
+ ip_class: Optional[str] = None,
271
+ session_id: Optional[str] = None,
272
+ allow_http: bool = False,
273
+ max_chars: int = DEFAULT_MAX_CHARS,
274
+ ) -> Dict[str, Any]:
275
+ def go(c: Any) -> Dict[str, Any]:
276
+ res = c.proxy.fetch(
277
+ url,
278
+ method=str(method).upper(),
279
+ country=country or None,
280
+ ip_class=ip_class or None,
281
+ session_id=session_id or None,
282
+ allow_http=bool(allow_http),
283
+ )
284
+ limit = max(0, min(int(max_chars or DEFAULT_MAX_CHARS), _MAX_CHARS_CAP))
285
+ text = res.text()
286
+ return {
287
+ "status": res.status,
288
+ "statusText": res.status_text,
289
+ "headers": dict(res.headers),
290
+ "body": text[:limit],
291
+ "bodyTruncated": bool(res.truncated) or len(text) > limit,
292
+ }
293
+
294
+ return self._run(go)
295
+
296
+ def proxy_url(
297
+ self,
298
+ country: Optional[str] = None,
299
+ ip_class: Optional[str] = None,
300
+ session_id: Optional[str] = None,
301
+ allow_http: bool = False,
302
+ protocol: Optional[str] = None,
303
+ ) -> Dict[str, Any]:
304
+ def go(c: Any) -> Dict[str, Any]:
305
+ opts = dict(
306
+ country=country or None,
307
+ ip_class=ip_class or None,
308
+ session_id=session_id or None,
309
+ allow_http=bool(allow_http),
310
+ )
311
+ username = c.proxy.build_username(**opts)
312
+ url = c.proxy.build_url(protocol=protocol or None, **opts)
313
+ m = re.match(r"^([a-z0-9]+://[^:@/]+:)[^@]*(@.*)$", url)
314
+ masked = f"{m.group(1)}{_KEY_PLACEHOLDER}{m.group(2)}" if m else ""
315
+ return {
316
+ "username": username,
317
+ "url": masked,
318
+ "note": f"Replace {_KEY_PLACEHOLDER} with your API key from the DOMINUSNODE_API_KEY "
319
+ "environment variable. The key goes in the proxy PASSWORD, routing flags in the USERNAME.",
320
+ }
321
+
322
+ return self._run(go)
323
+
324
+ # ------------------------------------------------------------------ 3-5 network
325
+ def network_policy(self) -> Dict[str, Any]:
326
+ return self._run(lambda c: c.network.policy())
327
+
328
+ def network_pricing(self) -> Dict[str, Any]:
329
+ return self._run(lambda c: c.network.pricing())
330
+
331
+ def network_stats(self) -> Dict[str, Any]:
332
+ return self._run(lambda c: c.network.stats())
333
+
334
+ # ------------------------------------------------------------------ 6-9 wallet
335
+ def balance(self) -> Dict[str, Any]:
336
+ return self._run(lambda c: c.wallet.get_balance())
337
+
338
+ def transactions(self, limit: Optional[int] = 20, offset: Optional[int] = None) -> Dict[str, Any]:
339
+ return self._run(lambda c: c.wallet.get_transactions(limit=limit, offset=offset))
340
+
341
+ def topup_crypto(self, amount_cents: int, currency: str) -> Dict[str, Any]:
342
+ return self._run(lambda c: c.wallet.top_up_crypto(amount_cents, currency))
343
+
344
+ def check_payment(self, invoice_id: str) -> Dict[str, Any]:
345
+ return self._run(lambda c: c.wallet.get_crypto_status(invoice_id))
346
+
347
+ # ------------------------------------------------------------------ 10-12 usage/sessions
348
+ def usage(self, since: Optional[str] = None, until: Optional[str] = None, limit: Optional[int] = 20) -> Dict[str, Any]:
349
+ return self._run(lambda c: c.usage.get(from_=since or None, to=until or None, limit=limit))
350
+
351
+ def active_sessions(self) -> Dict[str, Any]:
352
+ return self._run(lambda c: c.sessions.get_active())
353
+
354
+ def report_session(self, session_id: str, reason: str) -> Dict[str, Any]:
355
+ return self._run(lambda c: c.sessions.report(session_id, reason))
356
+
357
+ # ------------------------------------------------------------------ 13-15 nodes
358
+ def node_list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> Dict[str, Any]:
359
+ return self._run(lambda c: c.nodes.list(limit=limit, offset=offset))
360
+
361
+ def node_tier(self) -> Dict[str, Any]:
362
+ return self._run(lambda c: c.nodes.tier())
363
+
364
+ def node_setup_guide(self) -> Dict[str, Any]:
365
+ return json.loads(json.dumps(NODE_SETUP_GUIDE))
366
+
367
+ # ------------------------------------------------------------------ 16-19 credits/payouts/x402
368
+ def credits_balance(self) -> Dict[str, Any]:
369
+ return self._run(lambda c: c.credits.balance())
370
+
371
+ def credits_convert(self, amount_musd: str, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
372
+ return {"status": "signing_required", "message": CREDITS_CONVERT_SIGNING_MESSAGE}
373
+
374
+ def payouts_list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> Dict[str, Any]:
375
+ return self._run(lambda c: c.payouts.list(limit=limit, offset=offset))
376
+
377
+ def x402_info(self) -> Dict[str, Any]:
378
+ return self._run(lambda c: c.x402.get_info())
379
+
380
+
381
+ # ---------------------------------------------------------------------- tool specs
382
+ def _obj(props: Dict[str, Any], required: Optional[List[str]] = None) -> Dict[str, Any]:
383
+ return {"type": "object", "properties": props, "required": required or [], "additionalProperties": False}
384
+
385
+
386
+ _COUNTRY = {"type": "string", "description": "ISO 3166-1 alpha-2 exit country, e.g. US, DE. Sanctioned countries (CU, IR, KP, RU, SY) are refused."}
387
+ _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."}
388
+ _SESSION = {"type": "string", "description": "Sticky session id (1-64 letters/digits) to keep the same exit node across requests."}
389
+ _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."}
390
+ _LIMIT = {"type": "integer", "minimum": 1, "maximum": 200, "description": "Maximum rows to return."}
391
+ _OFFSET = {"type": "integer", "minimum": 0, "description": "Rows to skip (pagination)."}
392
+
393
+ TOOL_SPECS: Dict[str, Dict[str, Any]] = {
394
+ "fetch": {
395
+ "description": (
396
+ "Fetch a URL through the DominusNode relay network (agent-contributed exit nodes). GET or HEAD only, "
397
+ "ports 80/443, no redirects followed, private/internal targets refused. https by default; plaintext "
398
+ "http:// needs allow_http=true and is visible to the exit node. Billed per GB from your prepaid wallet "
399
+ "at the live rate in network_pricing. Returns status, headers and the body (truncated to max_chars)."
400
+ ),
401
+ "parameters": _obj(
402
+ {
403
+ "url": {"type": "string", "description": "Target URL (https://... ; http:// only with allow_http)."},
404
+ "method": {"type": "string", "enum": ["GET", "HEAD"], "default": "GET", "description": "HTTP method."},
405
+ "country": _COUNTRY,
406
+ "ip_class": _IP_CLASS,
407
+ "session_id": _SESSION,
408
+ "allow_http": _ALLOW_HTTP,
409
+ "max_chars": {"type": "integer", "minimum": 1, "maximum": _MAX_CHARS_CAP, "default": DEFAULT_MAX_CHARS, "description": "Maximum body characters returned."},
410
+ },
411
+ ["url"],
412
+ ),
413
+ },
414
+ "proxy_url": {
415
+ "description": (
416
+ "Build a relay proxy URL for use with any HTTP or SOCKS5 client. Routing flags (country, class, sticky "
417
+ "session, allowhttp) go in the proxy username; the account credential is shown only as the placeholder "
418
+ "<DOMINUSNODE_API_KEY>, which the caller fills from its own environment."
419
+ ),
420
+ "parameters": _obj(
421
+ {
422
+ "country": _COUNTRY,
423
+ "ip_class": _IP_CLASS,
424
+ "session_id": _SESSION,
425
+ "allow_http": _ALLOW_HTTP,
426
+ "protocol": {"type": "string", "enum": ["https", "socks5", "http"], "default": "https",
427
+ "description": "https (default: TLS to the gateway), socks5, or http (plaintext gateway; local dev only)."},
428
+ }
429
+ ),
430
+ },
431
+ "network_policy": {
432
+ "description": "Relay network policy: allowed ports and methods, blocked targets and countries, node rules. Read before routing unusual traffic.",
433
+ "parameters": _obj({}),
434
+ },
435
+ "network_pricing": {
436
+ "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.",
437
+ "parameters": _obj({}),
438
+ },
439
+ "network_stats": {
440
+ "description": "Relay network size: online nodes by IP class and by country. Use it to pick a country/class with capacity.",
441
+ "parameters": _obj({}),
442
+ },
443
+ "balance": {
444
+ "description": "Prepaid wallet balance (cents) that pays for relay traffic.",
445
+ "parameters": _obj({}),
446
+ },
447
+ "transactions": {
448
+ "description": "Recent prepaid wallet transactions (top-ups and traffic charges).",
449
+ "parameters": _obj({"limit": _LIMIT, "offset": _OFFSET}),
450
+ },
451
+ "topup_crypto": {
452
+ "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.",
453
+ "parameters": _obj(
454
+ {
455
+ "amount_cents": {"type": "integer", "minimum": 1000, "maximum": 100000, "description": "Amount in US cents (1000 = $10)."},
456
+ "currency": {"type": "string", "description": "Crypto currency code, e.g. usdc, usdt, btc, eth, sol."},
457
+ },
458
+ ["amount_cents", "currency"],
459
+ ),
460
+ },
461
+ "check_payment": {
462
+ "description": "Status of a crypto top-up invoice created by topup_crypto.",
463
+ "parameters": _obj({"invoice_id": {"type": "string", "description": "Invoice UUID from topup_crypto."}}, ["invoice_id"]),
464
+ },
465
+ "usage": {
466
+ "description": "Relay traffic usage summary (bytes, cost, requests) with recent records for a date range.",
467
+ "parameters": _obj(
468
+ {
469
+ "since": {"type": "string", "description": "Start, ISO 8601 (e.g. 2026-01-01). Default: server default period."},
470
+ "until": {"type": "string", "description": "End, ISO 8601."},
471
+ "limit": _LIMIT,
472
+ }
473
+ ),
474
+ },
475
+ "active_sessions": {
476
+ "description": "Currently active relay sessions (id, start time, status).",
477
+ "parameters": _obj({}),
478
+ },
479
+ "report_session": {
480
+ "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.",
481
+ "parameters": _obj(
482
+ {
483
+ "session_id": {"type": "string", "description": "Session UUID (from active_sessions)."},
484
+ "reason": {"type": "string", "enum": ["blocked", "captcha", "rate_limited", "slow", "bad_content", "other"], "description": "What went wrong."},
485
+ },
486
+ ["session_id", "reason"],
487
+ ),
488
+ },
489
+ "node_list": {
490
+ "description": "Your own relay nodes (the IPs you contribute to earn credits): status, class, country, reputation, last seen.",
491
+ "parameters": _obj({"limit": {"type": "integer", "minimum": 1, "maximum": 500, "description": "Maximum nodes."}, "offset": _OFFSET}),
492
+ },
493
+ "node_tier": {
494
+ "description": "Your node operator tier, earning share and progress toward the next tier.",
495
+ "parameters": _obj({}),
496
+ },
497
+ "node_setup_guide": {
498
+ "description": "How to earn by contributing an IP: steps to register and run the DominusNode node agent, and the earning rules.",
499
+ "parameters": _obj({}),
500
+ },
501
+ "credits_balance": {
502
+ "description": "Node operator earnings in micro-USD: pending, available and frozen credits, tier and payout status.",
503
+ "parameters": _obj({}),
504
+ },
505
+ "credits_convert": {
506
+ "description": "Explains how to convert node credits into wallet balance. Conversion needs a wallet signature, so it runs through the DominusNode MCP server or SDK with a signer key file, not through this tool.",
507
+ "parameters": _obj(
508
+ {
509
+ "amount_musd": {"type": "string", "description": "Amount in micro-USD as a digit string (1000000 = $1; min 10000)."},
510
+ "idempotency_key": {"type": "string", "description": "Optional key to make a retry safe."},
511
+ },
512
+ ["amount_musd"],
513
+ ),
514
+ },
515
+ "payouts_list": {
516
+ "description": "Payouts of earned credits to your payout wallet.",
517
+ "parameters": _obj({"limit": _LIMIT, "offset": _OFFSET}),
518
+ },
519
+ "x402_info": {
520
+ "description": "x402 HTTP micropayment support: facilitators, networks, currencies and server-reported pricing.",
521
+ "parameters": _obj({}),
522
+ },
523
+ }
524
+
525
+ assert list(TOOL_SPECS) == TOOL_NAMES
@@ -0,0 +1,34 @@
1
+ """IronClaw MCP bridge configuration for the DominusNode MCP server (v2).
2
+
3
+ The MCP server (``@dominusnode/mcp-server``) runs locally over stdio and reads its key
4
+ from ``DOMINUSNODE_API_KEY``. The generated config references that variable by name;
5
+ it never embeds the key itself.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+
13
+ def generate_mcp_config(base_url: Optional[str] = None) -> Dict[str, Any]:
14
+ """Return an IronClaw MCP server entry that launches the DominusNode MCP server.
15
+
16
+ Args:
17
+ base_url: Optional API base URL override, passed as ``DOMINUSNODE_API_URL``.
18
+ """
19
+ env: Dict[str, str] = {"DOMINUSNODE_API_KEY": "${DOMINUSNODE_API_KEY}"}
20
+ if base_url:
21
+ env["DOMINUSNODE_API_URL"] = base_url.rstrip("/")
22
+ return {
23
+ "server": {
24
+ "name": "dominusnode",
25
+ "description": "DominusNode agent relay network: fetch through relay nodes, pricing, wallet, node earnings",
26
+ "transport": {
27
+ "type": "stdio",
28
+ "command": "npx",
29
+ "args": ["-y", "@dominusnode/mcp-server"],
30
+ "env": env,
31
+ },
32
+ },
33
+ "capabilities": {"tools": True, "resources": False, "prompts": False},
34
+ }
@@ -0,0 +1,51 @@
1
+ """IronClaw Python-bridge tool definitions for the DominusNode v2 standard tool set.
2
+
3
+ IronClaw's Python bridge consumes tool dicts of the form
4
+ ``{"name", "description", "parameters" (JSON Schema), "handler"}``; handlers return
5
+ JSON strings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Callable, Dict, List, Optional
11
+
12
+ from dominusnode_ironclaw._core import TOOL_NAMES, TOOL_SPECS
13
+ from dominusnode_ironclaw._core import DominusNodeToolkit as _CoreToolkit
14
+ from dominusnode_ironclaw.mcp_config import generate_mcp_config
15
+
16
+ TOOL_PREFIX = "dominusnode_"
17
+
18
+
19
+ class DominusNodeToolkit(_CoreToolkit):
20
+ """The 19 DominusNode tools as IronClaw bridge tool definitions.
21
+
22
+ The API key is read from ``DOMINUSNODE_API_KEY`` (or the ``api_key`` argument);
23
+ it is never a tool parameter and never appears in a tool result.
24
+ """
25
+
26
+ def _handler(self, name: str) -> Callable[..., str]:
27
+ def run(**kwargs: Any) -> str:
28
+ return self.call_json(name, kwargs)
29
+
30
+ run.__name__ = TOOL_PREFIX + name
31
+ return run
32
+
33
+ def get_tools(self) -> List[Dict[str, Any]]:
34
+ return [
35
+ {
36
+ "name": TOOL_PREFIX + name,
37
+ "description": TOOL_SPECS[name]["description"],
38
+ "parameters": TOOL_SPECS[name]["parameters"],
39
+ "handler": self._handler(name),
40
+ }
41
+ for name in TOOL_NAMES
42
+ ]
43
+
44
+ def execute(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
45
+ """Run a tool by its bridge name (``dominusnode_<tool>``) and return a JSON string."""
46
+ name = tool_name[len(TOOL_PREFIX):] if tool_name.startswith(TOOL_PREFIX) else tool_name
47
+ return self.call_json(name, arguments)
48
+
49
+ @staticmethod
50
+ def mcp_config(base_url: Optional[str] = None) -> Dict[str, Any]:
51
+ return generate_mcp_config(base_url)
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: dominusnode-ironclaw
3
+ Version: 2.2.0
4
+ Summary: DominusNode agent relay network tools for IronClaw (19 tools, built on the dominusnode SDK)
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/0xcircuitbreaker/DomiNode/tree/master/integrations/ironclaw
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/ironclaw/CHANGELOG.md
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: dominusnode<3,>=2.2.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # dominusnode-ironclaw
19
+
20
+ DominusNode tools for [IronClaw](#usage): 19 tools for the DominusNode **agent relay network**.
21
+ Agents fetch the web through exit nodes contributed by other agents, pay per GB from a prepaid
22
+ wallet at the live price from `network_pricing`, and can earn credits by contributing their own IP
23
+ with the node agent (`node_setup_guide`).
24
+
25
+ Version 2 is built on the official Python SDK [`dominusnode`](../../sdks/python) (v2). All HTTP,
26
+ SSRF and DNS-rebinding checks, proxy tunnelling and credential scrubbing come from the SDK.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install dominusnode-ironclaw
32
+ # from source, with the local SDK
33
+ pip install -e ../../sdks/python -e ".[dev]"
34
+ ```
35
+
36
+ Defaults: API `https://api.dominusnode.com`, relay gateway `proxy.dominusnode.com`. Override with `DOMINUSNODE_BASE_URL` / `DOMINUSNODE_PROXY_HOST`. Proxy traffic uses TLS to the gateway (HTTPS proxy, port 8443). For a self-hosted coordinator set `DOMINUSNODE_PROXY_URL=https://<proxy-host>:8443`; `DOMINUSNODE_PROXY_INSECURE=1` selects plaintext 8080 for local development only (production gateways refuse it).
37
+
38
+ Set your key in the environment (it is never a tool argument and never appears in tool output):
39
+
40
+ ```bash
41
+ export DOMINUSNODE_API_KEY=dn_live_...
42
+ # optional: DOMINUSNODE_BASE_URL, DOMINUSNODE_PROXY_HOST
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from dominusnode_ironclaw import DominusNodeToolkit, generate_mcp_config
49
+
50
+ toolkit = DominusNodeToolkit() # key from DOMINUSNODE_API_KEY
51
+ tools = toolkit.get_tools() # register with the IronClaw Python bridge
52
+ print(toolkit.execute("dominusnode_network_pricing"))
53
+
54
+ # Or use IronClaw's MCP bridge with the DominusNode MCP server (stdio, key by env reference only):
55
+ config = generate_mcp_config()
56
+ ```
57
+
58
+ Tools are exposed as `dominusnode_<tool>` bridge tool dicts (`name`, `description`, `parameters`, `handler`).
59
+
60
+ ## The 19 tools
61
+
62
+ | # | Tool | SDK call | What it does |
63
+ |---|---|---|---|
64
+ | 1 | `fetch` | `proxy.fetch` | GET/HEAD a URL through the relay (country, ip_class, session_id, allow_http) |
65
+ | 2 | `proxy_url` | `proxy.build_url` | Proxy URL/username for any client; key shown only as `<DOMINUSNODE_API_KEY>` |
66
+ | 3 | `network_policy` | `network.policy` | Allowed ports/methods, blocked targets and countries |
67
+ | 4 | `network_pricing` | `network.pricing` | Live per-GB prices by IP class and node earning shares |
68
+ | 5 | `network_stats` | `network.stats` | Online nodes by class and country |
69
+ | 6 | `balance` | `wallet.get_balance` | Prepaid wallet balance |
70
+ | 7 | `transactions` | `wallet.get_transactions` | Prepaid wallet transactions |
71
+ | 8 | `topup_crypto` | `wallet.top_up_crypto` | Crypto top-up invoice ($10-$1,000) |
72
+ | 9 | `check_payment` | `wallet.get_crypto_status` | Crypto invoice status |
73
+ | 10 | `usage` | `usage.get` | Traffic usage summary and records |
74
+ | 11 | `active_sessions` | `sessions.get_active` | Active relay sessions |
75
+ | 12 | `report_session` | `sessions.report` | Report a bad exit session |
76
+ | 13 | `node_list` | `nodes.list` | Your contributed nodes |
77
+ | 14 | `node_tier` | `nodes.tier` | Node operator tier and progress |
78
+ | 15 | `node_setup_guide` | `(static)` | How to earn by running the node agent |
79
+ | 16 | `credits_balance` | `credits.balance` | Earned credits (µUSD) |
80
+ | 17 | `credits_convert` | none (signed flow) | Points to the MCP server or SDK (needs a wallet signature) |
81
+ | 18 | `payouts_list` | `payouts.list` | Payouts of earned credits |
82
+ | 19 | `x402_info` | `x402.get_info` | x402 micropayment info |
83
+
84
+ ## Security
85
+
86
+ - `fetch` is GET/HEAD only, ports 80/443, no redirects, 10 MB cap, private and internal targets
87
+ refused after DNS resolution (all addresses). `https` by default; plaintext `http://` needs
88
+ `allow_http=true` and is visible to the exit node.
89
+ - Sanctioned countries (CU, IR, KP, RU, SY) are refused.
90
+ - Every error and result is scrubbed of `dn_live_`/`dn_test_`/`dn_node_`/`dn_proxy_` keys and JWTs.
91
+ - Signing flows (node registration, withdrawals, payout wallet) are not exposed as LLM tools; use the
92
+ DominusNode MCP server or the SDK with a signer key file.
93
+
94
+ ## Tests
95
+
96
+ ```bash
97
+ pip install -e ../../sdks/python -e ".[dev]"
98
+ pytest
99
+ ```
@@ -0,0 +1,14 @@
1
+ CHANGELOG.md
2
+ LICENSE
3
+ MANIFEST.in
4
+ README.md
5
+ pyproject.toml
6
+ dominusnode_ironclaw/__init__.py
7
+ dominusnode_ironclaw/_core.py
8
+ dominusnode_ironclaw/mcp_config.py
9
+ dominusnode_ironclaw/tools.py
10
+ dominusnode_ironclaw.egg-info/PKG-INFO
11
+ dominusnode_ironclaw.egg-info/SOURCES.txt
12
+ dominusnode_ironclaw.egg-info/dependency_links.txt
13
+ dominusnode_ironclaw.egg-info/requires.txt
14
+ dominusnode_ironclaw.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ dominusnode<3,>=2.2.0
2
+
3
+ [dev]
4
+ pytest>=7.0
@@ -0,0 +1 @@
1
+ dominusnode_ironclaw
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dominusnode-ironclaw"
7
+ version = "2.2.0"
8
+ description = "DominusNode agent relay network tools for IronClaw (19 tools, built on the dominusnode SDK)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["dominusnode>=2.2.0,<3"]
13
+
14
+ [project.optional-dependencies]
15
+ dev = ["pytest>=7.0"]
16
+
17
+ [tool.setuptools.packages.find]
18
+ include = ["dominusnode_ironclaw*"]
19
+
20
+ [tool.pytest.ini_options]
21
+ testpaths = ["tests"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/0xcircuitbreaker/DomiNode/tree/master/integrations/ironclaw"
25
+ Repository = "https://github.com/0xcircuitbreaker/DomiNode"
26
+ Issues = "https://github.com/0xcircuitbreaker/DomiNode/issues"
27
+ Changelog = "https://github.com/0xcircuitbreaker/DomiNode/blob/master/integrations/ironclaw/CHANGELOG.md"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+