blitzwing 0.1.8 → 0.1.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzwing",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Interactive setup wizard to join a Blitzwing Petals mother swarm as a compute contributor",
5
5
  "bin": {
6
6
  "blitzwing": "bin/blitzwing.js"
package/src/install.js CHANGED
@@ -220,6 +220,7 @@ from pydantic import BaseModel, Field
220
220
 
221
221
  class ReloadRequest(BaseModel):
222
222
  block_indices: str = Field(..., pattern=r"^\\d+:\\d+$")
223
+ initial_peers: Optional[List[str]] = None
223
224
 
224
225
  class StatusResponse(BaseModel):
225
226
  running: bool
@@ -259,6 +260,13 @@ class Mgr:
259
260
  c += ["--public_ip", self.public_ip]
260
261
  if self.initial_peers:
261
262
  c += ["--initial_peers", *self.initial_peers]
263
+ use_auto_relay = os.environ.get("PETALS_USE_AUTO_RELAY", "1") not in ("0", "false", "False")
264
+ if not self.announce_maddrs and use_auto_relay:
265
+ pass
266
+ elif not use_auto_relay:
267
+ c += ["--no_auto_relay"]
268
+ if os.environ.get("PETALS_SKIP_REACHABILITY_CHECK", "1") in ("1", "true", "True"):
269
+ c += ["--skip_reachability_check"]
262
270
  return c
263
271
 
264
272
  def start(self, bi=None):
@@ -296,7 +304,9 @@ class Mgr:
296
304
  self.last_exit_code = p.returncode
297
305
  self._proc = None
298
306
 
299
- def reload(self, bi):
307
+ def reload(self, bi, initial_peers=None):
308
+ if initial_peers is not None:
309
+ self.initial_peers = initial_peers
300
310
  self.stop(); time.sleep(1); self.start(bi)
301
311
 
302
312
  def status(self):
@@ -333,7 +343,7 @@ def status():
333
343
  def reload(body: ReloadRequest):
334
344
  a,b = map(int, body.block_indices.split(":"))
335
345
  if b <= a: raise HTTPException(400, "bad range")
336
- mgr.reload(body.block_indices)
346
+ mgr.reload(body.block_indices, initial_peers=body.initial_peers)
337
347
  time.sleep(0.5)
338
348
  return mgr.status()
339
349
 
package/src/net.js CHANGED
@@ -24,6 +24,59 @@ export async function detectPublicIp() {
24
24
  return "";
25
25
  }
26
26
 
27
+ /** Local LAN/WSL IP for shard_manager_url metadata (not Petals announce). */
28
+ export async function detectLocalIp() {
29
+ if (process.env.BLITZWING_LOCAL_IP) {
30
+ return process.env.BLITZWING_LOCAL_IP.trim();
31
+ }
32
+ try {
33
+ const { networkInterfaces } = await import("node:os");
34
+ const nets = networkInterfaces();
35
+ for (const name of Object.keys(nets)) {
36
+ for (const net of nets[name] || []) {
37
+ if (net.family === "IPv4" && !net.internal) {
38
+ return net.address;
39
+ }
40
+ }
41
+ }
42
+ } catch {
43
+ /* ignore */
44
+ }
45
+ return "127.0.0.1";
46
+ }
47
+
48
+ /** Optional ngrok TCP tunnel for dev; production should use VM public IP or auto-relay. */
49
+ export async function discoverNgrokTcpAnnounce(petalsPort) {
50
+ const override = process.env.BLITZWING_ANNOUNCE_MADDRS?.trim();
51
+ if (override) return override;
52
+
53
+ try {
54
+ const res = await fetch("http://127.0.0.1:4040/api/tunnels", {
55
+ signal: AbortSignal.timeout(2000),
56
+ });
57
+ if (!res.ok) return null;
58
+ const data = await res.json();
59
+ const tunnel = (data.tunnels || []).find((t) => t.proto === "tcp");
60
+ if (!tunnel?.public_url) return null;
61
+ const url = new URL(tunnel.public_url);
62
+ return `/dns4/${url.hostname}/tcp/${url.port || petalsPort}`;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ export function extractPeerMultiaddrFromLog(logPath, fs) {
69
+ if (!fs.existsSync(logPath)) return null;
70
+ const text = fs.readFileSync(logPath, "utf8");
71
+ const line = text
72
+ .split("\n")
73
+ .reverse()
74
+ .find((l) => l.includes("Running a server on"));
75
+ if (!line) return null;
76
+ const m = line.match(/Running a server on \['([^']+)'\]/);
77
+ return m ? m[1] : null;
78
+ }
79
+
27
80
  export function which(cmd) {
28
81
  try {
29
82
  const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
package/src/wizard.js CHANGED
@@ -5,7 +5,12 @@ import path from "node:path";
5
5
  import { spawn } from "node:child_process";
6
6
  import { DEFAULT_DISCOVERY_URL, HOME_DIR, SHARD_PORT, PETALS_PORT } from "./config.js";
7
7
  import { listMothers, motherHosts, joinHost, readyHost, leaveHost } from "./api.js";
8
- import { detectPublicIp } from "./net.js";
8
+ import {
9
+ detectPublicIp,
10
+ detectLocalIp,
11
+ discoverNgrokTcpAnnounce,
12
+ extractPeerMultiaddrFromLog,
13
+ } from "./net.js";
9
14
  import {
10
15
  ensureVenvAndPetals,
11
16
  startShardManagerProcess,
@@ -55,6 +60,9 @@ ${color.bold("blitzwing")} — join a Blitzwing mother swarm as a compute node
55
60
 
56
61
  Env:
57
62
  BLITZWING_DISCOVERY_URL Override discovery service (default ${DEFAULT_DISCOVERY_URL})
63
+ BLITZWING_ANNOUNCE_MADDRS Explicit Petals announce multiaddr (VM with public IP)
64
+ BLITZWING_LOCAL_IP Local IP for shard manager metadata
65
+ PETALS_USE_AUTO_RELAY=0 Disable libp2p auto-relay (use with public IP / port-forward)
58
66
  `);
59
67
  }
60
68
 
@@ -130,18 +138,70 @@ async function wizard(args) {
130
138
  const layersN = Number(layers);
131
139
 
132
140
  const detected = await detectPublicIp();
133
- const publicIp = await p.text({
134
- message: "Public IP for this machine (other peers must reach you)",
135
- initialValue: detected || "",
136
- validate(v) {
137
- if (!v || !v.trim()) return "Public IP is required";
138
- },
141
+ const networkMode = await p.select({
142
+ message: "How will other peers reach your Petals node?",
143
+ options: [
144
+ {
145
+ value: "relay",
146
+ label: "Auto (recommended for home / NAT)",
147
+ hint: "Uses Petals libp2p relay — no port forwarding",
148
+ },
149
+ {
150
+ value: "public",
151
+ label: "Public IP / cloud VM",
152
+ hint: "TCP 31337 (and 8001) open on the internet",
153
+ },
154
+ {
155
+ value: "ngrok",
156
+ label: "Dev tunnel (ngrok TCP on :4040)",
157
+ hint: "Only if ngrok is already running",
158
+ },
159
+ ],
160
+ initialValue: "relay",
139
161
  });
140
- if (p.isCancel(publicIp)) {
162
+ if (p.isCancel(networkMode)) {
141
163
  p.cancel("Setup cancelled");
142
164
  process.exit(0);
143
165
  }
144
166
 
167
+ let publicIp = "";
168
+ let announceMaddrs = "";
169
+ let useAutoRelay = "1";
170
+
171
+ if (networkMode === "public") {
172
+ publicIp = await p.text({
173
+ message: "Public IPv4 (must be reachable on TCP " + PETALS_PORT + ")",
174
+ initialValue: detected || "",
175
+ validate(v) {
176
+ if (!v || !/^\d+\.\d+\.\d+\.\d+$/.test(String(v).trim())) {
177
+ return "Enter a valid public IPv4 address";
178
+ }
179
+ },
180
+ });
181
+ if (p.isCancel(publicIp)) {
182
+ p.cancel("Setup cancelled");
183
+ process.exit(0);
184
+ }
185
+ publicIp = String(publicIp).trim();
186
+ announceMaddrs = `/ip4/${publicIp}/tcp/${PETALS_PORT}`;
187
+ useAutoRelay = "0";
188
+ } else if (networkMode === "ngrok") {
189
+ const ngrok = await discoverNgrokTcpAnnounce(PETALS_PORT);
190
+ if (!ngrok) {
191
+ p.cancel("No ngrok TCP tunnel on http://127.0.0.1:4040. Start: ngrok tcp " + PETALS_PORT);
192
+ process.exit(1);
193
+ }
194
+ announceMaddrs = ngrok;
195
+ publicIp = ngrok.match(/dns4\/([^/]+)/)?.[1] || detected || "relay";
196
+ useAutoRelay = "0";
197
+ p.log.info(`Using ngrok announce ${announceMaddrs}`);
198
+ } else {
199
+ publicIp = detected || "relay";
200
+ p.log.info("Using Petals libp2p auto-relay (no inbound port forward required).");
201
+ }
202
+
203
+ const localIp = await detectLocalIp();
204
+
145
205
  const hederaAccount = await p.text({
146
206
  message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
147
207
  initialValue: process.env.BLITZWING_HEDERA_ACCOUNT_ID || "",
@@ -157,7 +217,7 @@ async function wizard(args) {
157
217
  const hederaAccountId = String(hederaAccount).trim();
158
218
 
159
219
  const confirm = await p.confirm({
160
- message: `Join ${selected.model} hosting ${layersN} layers from ${String(publicIp).trim()} paying to ${hederaAccountId}?`,
220
+ message: `Join ${selected.model} hosting ${layersN} layers (${networkMode}) paying to ${hederaAccountId}?`,
161
221
  initialValue: true,
162
222
  });
163
223
  if (p.isCancel(confirm) || !confirm) {
@@ -180,7 +240,8 @@ async function wizard(args) {
180
240
  const python = venvPython();
181
241
  spin.stop("Environment ready");
182
242
 
183
- const shardManagerUrl = `http://${String(publicIp).trim()}:${SHARD_PORT}`;
243
+ const shardManagerUrl = `http://${localIp}:${SHARD_PORT}`;
244
+ const petalsLogPath = path.join(HOME_DIR, "petals.log");
184
245
 
185
246
  spin.start("Requesting layer assignment from mother…");
186
247
  let assignment;
@@ -202,22 +263,28 @@ async function wizard(args) {
202
263
 
203
264
  const logPath = path.join(HOME_DIR, "shard_manager.log");
204
265
  spin.start("Starting local Petals server…");
205
- const { pid } = startShardManagerProcess({
206
- python,
207
- logPath,
208
- env: {
266
+ const shardEnv = {
209
267
  MODEL_NAME: selected.model,
210
- PUBLIC_IP: String(publicIp).trim(),
268
+ PUBLIC_IP: announceMaddrs ? "" : String(publicIp).trim(),
211
269
  BLOCK_INDICES: assignment.block_indices,
212
270
  INITIAL_PEERS: (assignment.initial_peers || []).join(","),
213
271
  PETALS_PORT: String(PETALS_PORT),
214
272
  IDENTITY_PATH: path.join(HOME_DIR, "petals-identity"),
215
273
  SHARD_AUTO_START: "1",
216
- },
274
+ PETALS_USE_AUTO_RELAY: useAutoRelay,
275
+ PETALS_SKIP_REACHABILITY_CHECK: "1",
276
+ };
277
+ if (announceMaddrs) {
278
+ shardEnv.ANNOUNCE_MADDRS = announceMaddrs;
279
+ }
280
+ const { pid } = startShardManagerProcess({
281
+ python,
282
+ logPath,
283
+ env: shardEnv,
217
284
  });
218
285
 
219
286
  try {
220
- await waitForShardRunning({ timeoutMs: 600000, statusHost: String(publicIp).trim() });
287
+ await waitForShardRunning({ timeoutMs: 600000, statusHost: "127.0.0.1" });
221
288
  } catch (err) {
222
289
  spin.stop("Petals did not become ready");
223
290
  p.cancel(`${err.message}. See ${logPath}`);
@@ -226,8 +293,12 @@ async function wizard(args) {
226
293
  spin.stop("Petals is serving your layers");
227
294
 
228
295
  spin.start("Finalizing handoff with mother…");
296
+ const peerMultiaddr = extractPeerMultiaddrFromLog(petalsLogPath, fs);
229
297
  try {
230
- await readyHost(selected.mother_url, { host_id: assignment.host_id });
298
+ await readyHost(selected.mother_url, {
299
+ host_id: assignment.host_id,
300
+ peer_multiaddr: peerMultiaddr || undefined,
301
+ });
231
302
  } catch (err) {
232
303
  spin.stop("Handoff failed");
233
304
  p.cancel(err.message);
@@ -242,6 +313,8 @@ async function wizard(args) {
242
313
  block_indices: assignment.block_indices,
243
314
  layers_hosted: assignment.layers_hosted,
244
315
  public_ip: String(publicIp).trim(),
316
+ network_mode: networkMode,
317
+ announce_maddrs: announceMaddrs || null,
245
318
  shard_manager_url: shardManagerUrl,
246
319
  shard_pid: pid,
247
320
  discovery_url: args.discoveryUrl,
@@ -297,7 +370,7 @@ async function showStatus() {
297
370
  console.log(` mother: ${state.mother_url}`);
298
371
  console.log(` public_ip: ${state.public_ip}`);
299
372
  try {
300
- const statusHost = state.public_ip || "127.0.0.1";
373
+ const statusHost = "127.0.0.1";
301
374
  const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
302
375
  if (res.ok) {
303
376
  const st = await res.json();
@@ -325,7 +398,7 @@ async function doLeave() {
325
398
  p.log.warn(`Mother leave call failed: ${err.message}`);
326
399
  }
327
400
  try {
328
- const statusHost = state.public_ip || "127.0.0.1";
401
+ const statusHost = "127.0.0.1";
329
402
  await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
330
403
  } catch {
331
404
  /* ignore */