flashnode 0.3.0__tar.gz → 0.3.2__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.
- {flashnode-0.3.0 → flashnode-0.3.2}/PKG-INFO +40 -2
- {flashnode-0.3.0 → flashnode-0.3.2}/README.md +38 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/agent/cli.py +91 -4
- flashnode-0.3.2/flashnode/doctor.py +533 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/argv_runner.py +4 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/docker_runner.py +5 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/hardening.py +33 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/loop.py +101 -1
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/inventory/capabilities.py +17 -1
- flashnode-0.3.2/flashnode/inventory/gpu.py +155 -0
- flashnode-0.3.2/flashnode/status.py +103 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode.egg-info/PKG-INFO +40 -2
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode.egg-info/SOURCES.txt +10 -1
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode.egg-info/requires.txt +1 -1
- {flashnode-0.3.0 → flashnode-0.3.2}/pyproject.toml +11 -2
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_agent.py +14 -15
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_capabilities.py +26 -0
- flashnode-0.3.2/tests/test_doctor.py +530 -0
- flashnode-0.3.2/tests/test_gpu_probe.py +176 -0
- flashnode-0.3.2/tests/test_hardening.py +203 -0
- flashnode-0.3.2/tests/test_loop_counters.py +223 -0
- flashnode-0.3.2/tests/test_status.py +125 -0
- flashnode-0.3.2/tests/test_work_gate.py +96 -0
- flashnode-0.3.2/tests/test_work_status.py +129 -0
- flashnode-0.3.0/tests/test_hardening.py +0 -44
- {flashnode-0.3.0 → flashnode-0.3.2}/LICENSE +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/agent/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/agent/daemon.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/agent/kube.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/artifacts/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/benchmark/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/config/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/config/local_data.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/archives.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/client.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/images.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/executor/runner.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/identity/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/identity/credentials.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/identity/enrol.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/identity/store.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/inventory/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode/telemetry/__init__.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode.egg-info/dependency_links.txt +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode.egg-info/entry_points.txt +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/flashnode.egg-info/top_level.txt +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/setup.cfg +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_allowlist_drift.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_archives.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_argv_runner.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_checkpoint_relay.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_credentials.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_docker_runner.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_enrol.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_executor.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_hardening_platform.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_identity.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_images_allowlist.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_interfaces.py +0 -0
- {flashnode-0.3.0 → flashnode-0.3.2}/tests/test_local_data.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashnode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Open host agent for the FlashML fragmented-compute network: join, benchmark, execute sandboxed ML tasks, earn contribution credits.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/Zolli-Labs/flashnode
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist: flashruntime<0.5,>=0.4
|
|
10
|
+
Requires-Dist: flashruntime<0.5,>=0.4.1
|
|
11
11
|
Requires-Dist: psutil>=5.9
|
|
12
12
|
Requires-Dist: websockets>=12
|
|
13
13
|
Requires-Dist: cryptography>=42
|
|
@@ -49,10 +49,48 @@ verified results. Two profiles:
|
|
|
49
49
|
inside managed pools (DaemonSet); KubeRay owns workload pods there.
|
|
50
50
|
Implemented.
|
|
51
51
|
|
|
52
|
+
## Check your machine first
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
flashnode doctor
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Six checks: the `docker` CLI, the engine behind it, an anonymous pull of a
|
|
59
|
+
curated image, whether a container can see your work directory, whether your
|
|
60
|
+
Docker accepts the sandbox flags, and whether any directories you lend via
|
|
61
|
+
`FLASHNODE_LOCAL_DATA` are readable. Every failure names the fix.
|
|
62
|
+
|
|
63
|
+
Run it once before `flashnode work`. `work` repeats all of it **except** the
|
|
64
|
+
image pull — a registry blip should not stop an agent whose images are
|
|
65
|
+
already cached — and refuses to start if anything fails, because a host that
|
|
66
|
+
cannot run tasks should not be claiming them.
|
|
67
|
+
|
|
68
|
+
## While it runs
|
|
69
|
+
|
|
70
|
+
On a terminal you get a live status block:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
flashnode 0.3.2 · flashml-api.onrender.com · up 2h14m
|
|
74
|
+
running fed-2e2d4d6ab57f · attempt 1 · 38s
|
|
75
|
+
session 12 accepted 0 failed
|
|
76
|
+
heartbeat 2s ago
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`waiting · no work queued — this is normal` means exactly that: the pool has
|
|
80
|
+
nothing for you right now, and your machine is fine. Pipe the output
|
|
81
|
+
anywhere, or pass `--log-json`, and you get the machine-readable log instead.
|
|
82
|
+
|
|
83
|
+
If three tasks in a row fail **on your machine**, the agent re-runs its own
|
|
84
|
+
checks. Pass, and the jobs were broken rather than your host, so it carries
|
|
85
|
+
on. Fail, and it stops claiming and tells you what to fix — instead of
|
|
86
|
+
burning a job's retries on a machine that cannot run anything.
|
|
87
|
+
`--max-consecutive-failures 0` turns that off.
|
|
88
|
+
|
|
52
89
|
## What it does today
|
|
53
90
|
|
|
54
91
|
```bash
|
|
55
92
|
pip install -e . # plus: pip install -e ../flashruntime
|
|
93
|
+
flashnode doctor # check this machine can run tasks
|
|
56
94
|
flashnode work --coordinator http://<coordinator>:8100
|
|
57
95
|
# optional hardening / pool config:
|
|
58
96
|
# FLASHNODE_JOIN_CODE=... join-code-gated pools
|
|
@@ -32,10 +32,48 @@ verified results. Two profiles:
|
|
|
32
32
|
inside managed pools (DaemonSet); KubeRay owns workload pods there.
|
|
33
33
|
Implemented.
|
|
34
34
|
|
|
35
|
+
## Check your machine first
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
flashnode doctor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Six checks: the `docker` CLI, the engine behind it, an anonymous pull of a
|
|
42
|
+
curated image, whether a container can see your work directory, whether your
|
|
43
|
+
Docker accepts the sandbox flags, and whether any directories you lend via
|
|
44
|
+
`FLASHNODE_LOCAL_DATA` are readable. Every failure names the fix.
|
|
45
|
+
|
|
46
|
+
Run it once before `flashnode work`. `work` repeats all of it **except** the
|
|
47
|
+
image pull — a registry blip should not stop an agent whose images are
|
|
48
|
+
already cached — and refuses to start if anything fails, because a host that
|
|
49
|
+
cannot run tasks should not be claiming them.
|
|
50
|
+
|
|
51
|
+
## While it runs
|
|
52
|
+
|
|
53
|
+
On a terminal you get a live status block:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
flashnode 0.3.2 · flashml-api.onrender.com · up 2h14m
|
|
57
|
+
running fed-2e2d4d6ab57f · attempt 1 · 38s
|
|
58
|
+
session 12 accepted 0 failed
|
|
59
|
+
heartbeat 2s ago
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`waiting · no work queued — this is normal` means exactly that: the pool has
|
|
63
|
+
nothing for you right now, and your machine is fine. Pipe the output
|
|
64
|
+
anywhere, or pass `--log-json`, and you get the machine-readable log instead.
|
|
65
|
+
|
|
66
|
+
If three tasks in a row fail **on your machine**, the agent re-runs its own
|
|
67
|
+
checks. Pass, and the jobs were broken rather than your host, so it carries
|
|
68
|
+
on. Fail, and it stops claiming and tells you what to fix — instead of
|
|
69
|
+
burning a job's retries on a machine that cannot run anything.
|
|
70
|
+
`--max-consecutive-failures 0` turns that off.
|
|
71
|
+
|
|
35
72
|
## What it does today
|
|
36
73
|
|
|
37
74
|
```bash
|
|
38
75
|
pip install -e . # plus: pip install -e ../flashruntime
|
|
76
|
+
flashnode doctor # check this machine can run tasks
|
|
39
77
|
flashnode work --coordinator http://<coordinator>:8100
|
|
40
78
|
# optional hardening / pool config:
|
|
41
79
|
# FLASHNODE_JOIN_CODE=... join-code-gated pools
|
|
@@ -26,6 +26,7 @@ commands:
|
|
|
26
26
|
(--coordinator URL; --token TOKEN to skip the browser step)
|
|
27
27
|
logout remove the saved bearer token for a FlashRuntime coordinator
|
|
28
28
|
(--coordinator URL)
|
|
29
|
+
doctor check this machine can run tasks (docker engine, images, mounts)
|
|
29
30
|
join connect this machine to a FlashML control plane (not yet implemented)
|
|
30
31
|
status show node identity, capabilities, and active leases (not yet implemented)
|
|
31
32
|
leave drain and disconnect (not yet implemented)
|
|
@@ -179,6 +180,16 @@ def _work(args: list[str]) -> int:
|
|
|
179
180
|
)
|
|
180
181
|
parser.add_argument("--max-tasks", type=int, default=None)
|
|
181
182
|
parser.add_argument("--poll-seconds", type=float, default=1.0)
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--log-json", action="store_true",
|
|
185
|
+
help="keep the machine-readable JSON log instead of the live status view",
|
|
186
|
+
)
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"--max-consecutive-failures", type=int,
|
|
189
|
+
default=int(os.environ.get("FLASHNODE_MAX_CONSECUTIVE_FAILURES", "3")),
|
|
190
|
+
help="host-side failures in a row before re-checking this machine and "
|
|
191
|
+
"stopping if it is broken (0 disables)",
|
|
192
|
+
)
|
|
182
193
|
opts = parser.parse_args(args)
|
|
183
194
|
|
|
184
195
|
runner = None
|
|
@@ -201,10 +212,35 @@ def _work(args: list[str]) -> int:
|
|
|
201
212
|
# (subprocess.run(["docker", ...])); if it isn't installed that call
|
|
202
213
|
# raises FileNotFoundError deep inside a task attempt. Check for it
|
|
203
214
|
# here, at startup, rather than let the agent die on the first task.
|
|
204
|
-
|
|
215
|
+
# A `docker` binary on PATH says nothing about the daemon behind it,
|
|
216
|
+
# the credential helper Docker consults when it pulls, or whether
|
|
217
|
+
# this machine's work directory is even visible inside the VM. Both
|
|
218
|
+
# hosts that stopped the 2026-08-02 §10 run-through passed the old
|
|
219
|
+
# `shutil.which` check and then failed every task they claimed —
|
|
220
|
+
# docker_runner raises TaskExecutionError, loop.py calls fail() and
|
|
221
|
+
# claims the next one, forever, silently.
|
|
222
|
+
#
|
|
223
|
+
# pull=False deliberately: an agent is a long-running daemon on
|
|
224
|
+
# someone else's machine, and a transient registry blip must not
|
|
225
|
+
# stop one whose images are already cached. `flashnode doctor` does
|
|
226
|
+
# the pull.
|
|
227
|
+
#
|
|
228
|
+
# The gate reads doctor's own NON_BLOCKING_STATUSES rather than
|
|
229
|
+
# testing `!= "ok"` here. Not every check is a gate: the GPU check is
|
|
230
|
+
# informational, because most volunteers have no GPU and blocking on
|
|
231
|
+
# it would lock the entire existing fleet out of CPU work the moment
|
|
232
|
+
# they upgraded. Keeping the predicate in one place is what stops
|
|
233
|
+
# `flashnode work` and `flashnode doctor` disagreeing about which
|
|
234
|
+
# verdicts are fatal.
|
|
235
|
+
from flashnode.doctor import NON_BLOCKING_STATUSES, format_results, run_checks
|
|
236
|
+
|
|
237
|
+
results = run_checks(pull=False)
|
|
238
|
+
if any(r.status not in NON_BLOCKING_STATUSES for r in results):
|
|
205
239
|
print(
|
|
206
|
-
f"flashnode work:
|
|
207
|
-
"
|
|
240
|
+
f"flashnode work: this machine cannot run tasks with "
|
|
241
|
+
f"--runner {opts.runner}.\n" + format_results(results)
|
|
242
|
+
+ "\n\nRun `flashnode doctor` for the full check, including "
|
|
243
|
+
"the image pull this skipped.",
|
|
208
244
|
file=sys.stderr,
|
|
209
245
|
)
|
|
210
246
|
return 2
|
|
@@ -246,10 +282,29 @@ def _work(args: list[str]) -> int:
|
|
|
246
282
|
module_capable=(opts.runner != "argv"),
|
|
247
283
|
)
|
|
248
284
|
client.register(registration)
|
|
285
|
+
from flashnode.doctor import NON_BLOCKING_STATUSES, run_checks
|
|
286
|
+
|
|
287
|
+
def _blocking_problems():
|
|
288
|
+
"""What the loop calls after a streak of host-side failures.
|
|
289
|
+
|
|
290
|
+
Filtered HERE, with the same set the startup gate reads, so the two
|
|
291
|
+
cannot drift — and so `loop.py` never has to import the doctor
|
|
292
|
+
(which would close a loop -> doctor -> executor -> loop cycle).
|
|
293
|
+
The GPU check reports "info" and never fails; a loop testing
|
|
294
|
+
`!= "ok"` itself would quarantine every CPU-only volunteer.
|
|
295
|
+
|
|
296
|
+
pull=False for the same reason the startup gate uses it: a registry
|
|
297
|
+
blip must not stop an agent whose images are already cached.
|
|
298
|
+
"""
|
|
299
|
+
return [r for r in run_checks(pull=False)
|
|
300
|
+
if r.status not in NON_BLOCKING_STATUSES]
|
|
301
|
+
|
|
249
302
|
loop = ExecutorLoop(
|
|
250
303
|
client, node_id, runner=runner,
|
|
251
304
|
poll_seconds=opts.poll_seconds, workdir_base=workdir_base,
|
|
252
305
|
registration=registration, # survives coordinator restarts
|
|
306
|
+
health_check=_blocking_problems,
|
|
307
|
+
max_consecutive_failures=opts.max_consecutive_failures,
|
|
253
308
|
)
|
|
254
309
|
|
|
255
310
|
def _stop(signum, frame): # noqa: ARG001
|
|
@@ -257,7 +312,35 @@ def _work(args: list[str]) -> int:
|
|
|
257
312
|
|
|
258
313
|
signal.signal(signal.SIGTERM, _stop)
|
|
259
314
|
signal.signal(signal.SIGINT, _stop)
|
|
260
|
-
|
|
315
|
+
view = None
|
|
316
|
+
if sys.stdout.isatty() and not opts.log_json:
|
|
317
|
+
# Two writers redrawing one terminal is unreadable, so the JSON
|
|
318
|
+
# handler goes when the view arrives. --log-json keeps it, and a
|
|
319
|
+
# non-TTY never gets here — ANSI cursor movement into a pipe or a
|
|
320
|
+
# systemd journal is corruption, not output.
|
|
321
|
+
from flashnode.status import StatusView
|
|
322
|
+
|
|
323
|
+
logging.getLogger().handlers.clear()
|
|
324
|
+
view = StatusView(loop, coordinator=opts.coordinator,
|
|
325
|
+
version=__version__, stream=sys.stdout)
|
|
326
|
+
view.start()
|
|
327
|
+
try:
|
|
328
|
+
accepted = loop.run(max_tasks=opts.max_tasks)
|
|
329
|
+
finally:
|
|
330
|
+
if view is not None:
|
|
331
|
+
view.stop()
|
|
332
|
+
|
|
333
|
+
if getattr(loop, "quarantined", False):
|
|
334
|
+
from flashnode.doctor import format_results
|
|
335
|
+
|
|
336
|
+
print(
|
|
337
|
+
"\nflashnode work: stopping — this machine can no longer run "
|
|
338
|
+
"tasks.\n" + format_results(loop.health_report or [])
|
|
339
|
+
+ "\n\nFix the above, then `flashnode doctor` to confirm before "
|
|
340
|
+
"restarting.",
|
|
341
|
+
file=sys.stderr,
|
|
342
|
+
)
|
|
343
|
+
return 2
|
|
261
344
|
print(f"flashnode work: {accepted} task(s) accepted", file=sys.stderr)
|
|
262
345
|
return 0
|
|
263
346
|
|
|
@@ -270,6 +353,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
270
353
|
return agent_main()
|
|
271
354
|
if args and args[0] == "work":
|
|
272
355
|
return _work(args[1:])
|
|
356
|
+
if args and args[0] == "doctor":
|
|
357
|
+
from flashnode.doctor import doctor_main
|
|
358
|
+
|
|
359
|
+
return doctor_main(args[1:])
|
|
273
360
|
if args and args[0] == "login":
|
|
274
361
|
return _login(args[1:])
|
|
275
362
|
if args and args[0] == "logout":
|