iowap-server 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. iowap_server-2.0.0.dist-info/METADATA +129 -0
  2. iowap_server-2.0.0.dist-info/RECORD +108 -0
  3. iowap_server-2.0.0.dist-info/WHEEL +5 -0
  4. iowap_server-2.0.0.dist-info/entry_points.txt +3 -0
  5. iowap_server-2.0.0.dist-info/licenses/LICENSE +21 -0
  6. iowap_server-2.0.0.dist-info/top_level.txt +1 -0
  7. relay_server/README.md +97 -0
  8. relay_server/__init__.py +3 -0
  9. relay_server/api/__init__.py +1 -0
  10. relay_server/api/v2/__init__.py +26 -0
  11. relay_server/api/v2/admin.py +236 -0
  12. relay_server/api/v2/auth.py +402 -0
  13. relay_server/api/v2/cluster.py +485 -0
  14. relay_server/api/v2/dashboard.py +1181 -0
  15. relay_server/api/v2/discovery.py +177 -0
  16. relay_server/api/v2/docs.py +197 -0
  17. relay_server/api/v2/events.py +63 -0
  18. relay_server/api/v2/presence.py +50 -0
  19. relay_server/api/v2/scheduler.py +276 -0
  20. relay_server/api/v2/security.py +249 -0
  21. relay_server/api/v2/storage.py +307 -0
  22. relay_server/cli.py +95 -0
  23. relay_server/config.py +234 -0
  24. relay_server/core/__init__.py +1 -0
  25. relay_server/core/artifacts.py +388 -0
  26. relay_server/core/auth.py +654 -0
  27. relay_server/core/chunked_upload.py +252 -0
  28. relay_server/core/db.py +1605 -0
  29. relay_server/core/db_postgres.py +80 -0
  30. relay_server/core/discovery.py +828 -0
  31. relay_server/core/events.py +178 -0
  32. relay_server/core/logging_setup.py +40 -0
  33. relay_server/core/maintenance.py +338 -0
  34. relay_server/core/metrics.py +351 -0
  35. relay_server/core/net_guard.py +238 -0
  36. relay_server/core/node_registry.py +235 -0
  37. relay_server/core/presence.py +180 -0
  38. relay_server/core/route_registry.py +506 -0
  39. relay_server/core/scheduler.py +1329 -0
  40. relay_server/core/session.py +79 -0
  41. relay_server/core/status.py +291 -0
  42. relay_server/core/tables.py +325 -0
  43. relay_server/core/users.py +422 -0
  44. relay_server/core/zeroconf.py +166 -0
  45. relay_server/docs/CONTRIBUTING.md +76 -0
  46. relay_server/docs/README.md +96 -0
  47. relay_server/docs/captures/README.md +43 -0
  48. relay_server/docs/captures/capabilities-20261016.md +1128 -0
  49. relay_server/docs/captures/health-20261016.md +14 -0
  50. relay_server/docs/captures/nodes-20261016.md +71 -0
  51. relay_server/docs/clients/mcp.md +86 -0
  52. relay_server/docs/concepts/artifacts.md +59 -0
  53. relay_server/docs/concepts/capabilities.md +132 -0
  54. relay_server/docs/concepts/flow.md +107 -0
  55. relay_server/docs/concepts/glossary.md +41 -0
  56. relay_server/docs/concepts/nodes.md +114 -0
  57. relay_server/docs/concepts/observability.md +71 -0
  58. relay_server/docs/concepts/overview.md +93 -0
  59. relay_server/docs/concepts/security.md +71 -0
  60. relay_server/docs/concepts/tasks.md +112 -0
  61. relay_server/docs/concepts/tokens.md +67 -0
  62. relay_server/docs/federation/concept.md +60 -0
  63. relay_server/docs/flow/runner.md +112 -0
  64. relay_server/docs/getting-started.md +94 -0
  65. relay_server/docs/node/capabilities.md +204 -0
  66. relay_server/docs/node/cli.md +86 -0
  67. relay_server/docs/node/config.md +109 -0
  68. relay_server/docs/node/handlers/contract.md +132 -0
  69. relay_server/docs/node/handlers/primitives.md +140 -0
  70. relay_server/docs/node/integrations/hermes.md +146 -0
  71. relay_server/docs/node/integrations/home-assistant.md +180 -0
  72. relay_server/docs/node/operations.md +100 -0
  73. relay_server/docs/node/setup.md +200 -0
  74. relay_server/docs/node/tokens.md +90 -0
  75. relay_server/docs/reference/api.md +382 -0
  76. relay_server/docs/reference/database-backends.md +129 -0
  77. relay_server/docs/reference/design-board.md +37 -0
  78. relay_server/docs/server/admin.md +114 -0
  79. relay_server/docs/server/dashboard.md +109 -0
  80. relay_server/docs/server/database.md +79 -0
  81. relay_server/docs/server/docker.md +127 -0
  82. relay_server/docs/server/setup.md +261 -0
  83. relay_server/docs/storage/qnap.md +95 -0
  84. relay_server/docs/storage/storage.md +120 -0
  85. relay_server/main.py +462 -0
  86. relay_server/models/__init__.py +488 -0
  87. relay_server/models/capability.py +131 -0
  88. relay_server/models/discovery.py +75 -0
  89. relay_server/models/task.py +191 -0
  90. relay_server/static/admin.css +562 -0
  91. relay_server/static/admin.html +238 -0
  92. relay_server/static/admin.js +812 -0
  93. relay_server/static/bootstrap.html +54 -0
  94. relay_server/static/bootstrap.js +93 -0
  95. relay_server/static/change-password.html +43 -0
  96. relay_server/static/change-password.js +47 -0
  97. relay_server/static/cluster.css +663 -0
  98. relay_server/static/dashboard.html +77 -0
  99. relay_server/static/dashboard.js +21 -0
  100. relay_server/static/login.html +64 -0
  101. relay_server/static/login.js +20 -0
  102. relay_server/static/metrics.html +98 -0
  103. relay_server/static/metrics.js +204 -0
  104. relay_server/static/node-profile.html +62 -0
  105. relay_server/static/node-profile.js +217 -0
  106. relay_server/static/user-profile.html +59 -0
  107. relay_server/static/user-profile.js +125 -0
  108. relay_server/utils/__init__.py +1 -0
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: iowap-server
3
+ Version: 2.0.0
4
+ Summary: IOWAP Relay Server — API, Scheduler, Auth, Dashboard, Metrics
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: fastapi>=0.110
10
+ Requires-Dist: uvicorn[standard]>=0.29
11
+ Requires-Dist: pydantic>=2.7
12
+ Requires-Dist: pydantic-settings>=2.2
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: pyyaml>=6.0
15
+ Requires-Dist: python-multipart>=0.0.18
16
+ Requires-Dist: bcrypt>=4.1
17
+ Requires-Dist: itsdangerous>=2.1
18
+ Requires-Dist: zeroconf>=0.63
19
+ Requires-Dist: slowapi>=0.1
20
+ Requires-Dist: markdown>=3.6
21
+ Requires-Dist: sqlalchemy>=2.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
25
+ Requires-Dist: httpx>=0.27; extra == "dev"
26
+ Requires-Dist: ruff>=0.4; extra == "dev"
27
+ Provides-Extra: uvloop
28
+ Requires-Dist: uvloop>=0.19; extra == "uvloop"
29
+ Provides-Extra: postgres
30
+ Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
31
+ Dynamic: license-file
32
+
33
+ # iowap-server
34
+
35
+ **IOWAP Relay Server — API, Scheduler, Auth, Dashboard, Metrics**
36
+
37
+ The relay server is the central coordinator in the IOWAP ecosystem. Nodes register their capabilities via heartbeat, and the relay matches tasks to capable nodes. It is a **purpose-agnostic scheduler** — it knows nothing about the work itself, only who can do what.
38
+
39
+ ## Features
40
+
41
+ - **Capability Registry** — nodes heartbeat their capabilities, relay maintains a live registry
42
+ - **Task Scheduler** — match tasks to capable nodes, claim/release lifecycle with retry
43
+ - **Auth & Security** — token-based node auth, CSRF-protected dashboard, TLS support
44
+ - **Dashboard** — web UI for node overview, task monitoring, capability browsing, metrics
45
+ - **SSE Events** — real-time event stream for connected nodes
46
+ - **Status System** — node/task/stage status registry with valid transitions
47
+ - **Event Bus** — publish/subscribe internal events for monitoring & extensibility
48
+ - **PostgreSQL + SQLite** — production (Postgres) and dev (SQLite) backends
49
+ - **Docker** — `docker compose up` for one-command deployment
50
+ - **Observability** — `/metrics` (Prometheus), `/ready` health checks, structured JSON logs
51
+
52
+ ## Quick Start (Linux)
53
+
54
+ ```bash
55
+ curl -fsSL https://raw.githubusercontent.com/iowap-org/iowap-server/main/install.sh | bash
56
+ ```
57
+
58
+ The installer asks **native (systemd)** vs. **Docker (GHCR image)**, configures the
59
+ database (SQLite default, PostgreSQL local/remote) and environment (homelab HTTP /
60
+ internet TLS), creates the master admin seed, and prints the dashboard login URL.
61
+
62
+ **Docker (manual)** — pre-built image on GHCR:
63
+
64
+ ```bash
65
+ docker volume create relay-data
66
+ docker run -d --name iowap-server --restart unless-stopped \
67
+ -p 8788:8788 \
68
+ -e RELAY_MASTER_SEED="$(openssl rand -hex 24)" \
69
+ -e RELAY_SESSION_SECRET="$(openssl rand -base64 32)" \
70
+ -e RELAY_SESSION_COOKIE_SECURE=false \
71
+ -v relay-data:/app/.relay \
72
+ ghcr.io/iowap-org/iowap-server:latest
73
+ ```
74
+
75
+ Compose files live in [iowap-org/iowap-docker](https://github.com/iowap-org/iowap-docker) (`server/docker-compose.yml`).
76
+
77
+ **Manual (source)**:
78
+
79
+ ```bash
80
+ git clone --recursive https://github.com/iowap-org/iowap-server.git
81
+ cd iowap-server
82
+ pip install -e . # or: pip install -e ".[postgres]"
83
+ relay-server --host 0.0.0.0 --port 8788
84
+ ```
85
+
86
+ Server starts on `http://localhost:8788` — the dashboard login is under
87
+ `/relay/v2/dashboard/login` (master seed acts as the bootstrap admin until the
88
+ first human admin exists).
89
+
90
+ ## Configuration
91
+
92
+ | Env | Default | Description |
93
+ |-----|---------|-------------|
94
+ | `RELAY_PORT` | 8788 | Server listen port |
95
+ | `RELAY_DB_TYPE` | sqlite | Database backend (sqlite / postgres) |
96
+ | `RELAY_MASTER_SEED` | — | Deterministic master seed for token generation |
97
+ | `TLS_CERTFILE` | — | TLS cert path (enables HTTPS, disables mDNS) |
98
+ | `POSTGRES_*` | — | PostgreSQL connection (when `RELAY_DB_TYPE=postgres`) |
99
+
100
+ See [iowap-docs → server/setup.md](https://github.com/iowap-org/iowap-docs/blob/main/server/setup.md) for full reference.
101
+
102
+ ## Architecture
103
+
104
+ ```
105
+ ┌──────────┐ heartbeat + capabilities ┌───────────┐
106
+ │ Nodes │ ◄─────────────────────────► │ Relay │
107
+ │ (any) │ claim + complete tasks │ Server │
108
+ └──────────┘ └─────┬─────┘
109
+ │
110
+ ┌───────┴───────┐
111
+ │ Dashboard │
112
+ │ (HTMX/HTML) │
113
+ └───────────────┘
114
+ ```
115
+
116
+ ## Docs
117
+
118
+ Full documentation in [iowap-org/iowap-docs](https://github.com/iowap-org/iowap-docs):
119
+
120
+ - [getting-started.md](https://github.com/iowap-org/iowap-docs/blob/main/getting-started.md) — first steps
121
+ - [server/setup.md](https://github.com/iowap-org/iowap-docs/blob/main/server/setup.md) — full setup guide
122
+ - [server/docker.md](https://github.com/iowap-org/iowap-docs/blob/main/server/docker.md) — Docker deployment
123
+ - [reference/api.md](https://github.com/iowap-org/iowap-docs/blob/main/reference/api.md) — API reference
124
+ - [server/dashboard.md](https://github.com/iowap-org/iowap-docs/blob/main/server/dashboard.md) — dashboard usage
125
+ - [server/admin.md](https://github.com/iowap-org/iowap-docs/blob/main/server/admin.md) — administration
126
+
127
+ ## License
128
+
129
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,108 @@
1
+ iowap_server-2.0.0.dist-info/licenses/LICENSE,sha256=jqWgc-nPTXXB5PYfNCScU8u8c1ixMcrg0SFZLcejJ8c,1068
2
+ relay_server/README.md,sha256=LJ8jzcBKDS4j6sb-jKeRbX--Ygb8aczELKR8kXWeiIk,4637
3
+ relay_server/__init__.py,sha256=QODLAIMc9b1Erd-uOVJhMyE9ZSpheiwHrdR2Hr3_Fzk,78
4
+ relay_server/cli.py,sha256=KrnG-UcItqpL8N2RDVF1oo1wLE--DWWmma_Ri7iWIss,3388
5
+ relay_server/config.py,sha256=hdwNuLSib2LNlPAZ5U9mdB5gHD4vehMzJ289pvEng7U,9307
6
+ relay_server/main.py,sha256=4bjTnkXQw6IIETY60cpshhveMnVRE2UPi2mmwer_2wI,18296
7
+ relay_server/api/__init__.py,sha256=AHgeCPa3roNWGhsU5Oo_c9NM-YJnU5fpmVYKF0_Hwgg,53
8
+ relay_server/api/v2/__init__.py,sha256=x_QwacN6wbuzeGyF4r-ID6TW2HtAeXM2FUf-XtZtCDk,1489
9
+ relay_server/api/v2/admin.py,sha256=svXHEiSjIsnJzxAjor27RwycBH6w5bLx7DijrZT8VpM,8047
10
+ relay_server/api/v2/auth.py,sha256=Y0wO9sak3EkgbNv70xzW0r_ibJlXVOHFK6xwCR2VCr4,14341
11
+ relay_server/api/v2/cluster.py,sha256=lATKqe4YnQd4DYlcaaCdlS1qOrgJX3NqZY-_WL8w_Bo,19007
12
+ relay_server/api/v2/dashboard.py,sha256=FcJT3LDCAQpYXivQLunWkZDjrRPbvbXR7zyqPi1pxzc,41563
13
+ relay_server/api/v2/discovery.py,sha256=F4RMCEx9F_qAFBTkaWk7__5FKP73M-_SJMbEhXjdGfA,5759
14
+ relay_server/api/v2/docs.py,sha256=HINHvfhjw_eegaXNVBu42faYAX0G-XtmWTeQEMhGnw8,7741
15
+ relay_server/api/v2/events.py,sha256=viyIlrHPstq-qbIQrm_FlGLf3aKvW1Ulzl8Z3g7Hyx0,1998
16
+ relay_server/api/v2/presence.py,sha256=ErXlIORIaV0sl_k7XHpJH-c8VwRMIRBt35cVgosWbGw,1543
17
+ relay_server/api/v2/scheduler.py,sha256=U581sjmWzfEdqHSRKFJEAgZD3hRzA0e2jfnYagS8zvw,9913
18
+ relay_server/api/v2/security.py,sha256=Fp-zNzsAHJEvTGpzVgeNhv2OiGNurbP8tXgnkk2-JDU,8617
19
+ relay_server/api/v2/storage.py,sha256=OtwPT7xbWTT_jnxNy7fAhsu8zTxqQrOfv1dmT0-sPwo,11279
20
+ relay_server/core/__init__.py,sha256=HdOQ7tmV-GKrY_m_PZpXSusDLX4uurFqboEuBV1Nisk,36
21
+ relay_server/core/artifacts.py,sha256=DvifTj6u_ZVLHN7P8-cOm6OWC5ZtDlAkoP2Hlr5vsnc,13263
22
+ relay_server/core/auth.py,sha256=4xIuOpkWnhy0gCbLVuDvHFI8EWhCGuFXMEegJONqKa4,21871
23
+ relay_server/core/chunked_upload.py,sha256=-AH7SNKGstP3BeMCszw9ST3P3tPSMizzr1c_f1g3zgo,9730
24
+ relay_server/core/db.py,sha256=YbaZBL4Y2c-WhYzIeAqai4Vdc_TjRItVYiN53OrXM9Y,63710
25
+ relay_server/core/db_postgres.py,sha256=byjLhWZIE5XitFEYbbCFwFeA89Ct8Uw5bAuGi1ICRxs,3114
26
+ relay_server/core/discovery.py,sha256=-LQQvhaHCAEthq5FLRnZPIMopnj_A9sR9Zi6bFhAATQ,34985
27
+ relay_server/core/events.py,sha256=Y40oyegjBpIYA-SZfRb97LhL96Uvp9mtu914rfzxqZk,6976
28
+ relay_server/core/logging_setup.py,sha256=m_suoLvAjZIesWS97msZt102DR5Mq0QjJ4N4Q1q1swk,1096
29
+ relay_server/core/maintenance.py,sha256=ftSJrQW3FL956x1wG8YUx2vCW8Xo6U5U5M5lTQChD6g,13372
30
+ relay_server/core/metrics.py,sha256=txw-5_tosvyolxeMZ4voTf-OweJQJ16QkZTFfVfVvws,13558
31
+ relay_server/core/net_guard.py,sha256=SGQmOP56JNo80awuna-s3aornVM5vLJZpJETREtZ4kc,9113
32
+ relay_server/core/node_registry.py,sha256=HF47AZHspof7PIBfx24BCI34lJDjzzvwqPzdT3yICvI,8835
33
+ relay_server/core/presence.py,sha256=sQPNai9vOS63wr7OOF0KQdQlUH-Opz-cDu5oC2oKNSM,5882
34
+ relay_server/core/route_registry.py,sha256=bE5ViJCum2_Nt9JarAS8Jvpl67wubczNvtuO_N8ybxg,20499
35
+ relay_server/core/scheduler.py,sha256=a3FXOcRqDS9znBsrVd247CaZSQNdTrDJ2_mth1Apg4U,56862
36
+ relay_server/core/session.py,sha256=pQLOQUArGzX_METWcxLI34rZz0wBsrRoOCEETOYHA1g,3171
37
+ relay_server/core/status.py,sha256=xOH5ec_-CjzL0wohi_2gCryYP__P3aKMFOlV9WaHiQ4,12884
38
+ relay_server/core/tables.py,sha256=dZulTnyTGF8L0fABECyTwyRSz46ohJU6222zTKPH5Wg,15270
39
+ relay_server/core/users.py,sha256=rUjzfANmmd0h_7C3tbLBa2Ba_8IAk8gvOqGxCgDyo9g,13632
40
+ relay_server/core/zeroconf.py,sha256=VYuhCr9tzZKckACCQgISXB-kqHJ7LPhqV3BRNcq85PY,5438
41
+ relay_server/docs/CONTRIBUTING.md,sha256=jR1bQUZwDLKxXRrv20McvZeELtSXzTVv39530h9KmgQ,3300
42
+ relay_server/docs/README.md,sha256=gb-2_gO44adGdKabcK5uFwu8IU6wPxo6bCcfjqDhI9w,4985
43
+ relay_server/docs/getting-started.md,sha256=zahuvcLu4Tu3UWz9zJij4soSBAASUQZZ3VW6THLmPjY,3896
44
+ relay_server/docs/captures/README.md,sha256=PkZlOtWalZe7vPgLXjyF2DlDX0CB1GVC31cszODi3NY,1816
45
+ relay_server/docs/captures/capabilities-20261016.md,sha256=OYm0Dfm5osEBq2Z3Us3FLx-WfI6wDaYJd3kMeNxZUQo,50191
46
+ relay_server/docs/captures/health-20261016.md,sha256=Dz17o64gq52kn5VrcZcLJzH3Jy65FnAYj1uQaa4yeyo,349
47
+ relay_server/docs/captures/nodes-20261016.md,sha256=JkIDsZ7FqZUneUveOjNvYBDQ0_BjNjK02mKNhy3mJDY,2829
48
+ relay_server/docs/clients/mcp.md,sha256=-07yS-n2TU-dgD4sXpI5tuVupEUCtqp0IYVbb3khxTE,3895
49
+ relay_server/docs/concepts/artifacts.md,sha256=RBPfCAem44yTCITlUb5ZArUo6aDFVkimcpC9tc-Kalw,2875
50
+ relay_server/docs/concepts/capabilities.md,sha256=ALPPIpSO65S1tDG3VVyAB2TgF5aKZaQkZdE06uCr2uQ,6468
51
+ relay_server/docs/concepts/flow.md,sha256=JsbdAYAbNsJj7IA7MYiWCM1QFHX7UpB82YglvB0QW9w,4916
52
+ relay_server/docs/concepts/glossary.md,sha256=1XA63JHH2EEGP0Iu0zmhiPRs26WZB-rSuhBcHc5CK-Q,4404
53
+ relay_server/docs/concepts/nodes.md,sha256=S-LhUsW22tRAfe80zRKVDSb0YBN33GiLlDG_Fp254Rk,5855
54
+ relay_server/docs/concepts/observability.md,sha256=LQsi3F7iRWrfUA5vY0vLDGf_6OPylEsV2zojc2Vviuo,2768
55
+ relay_server/docs/concepts/overview.md,sha256=yIGU5V16cKPM2SuhxHWfQ3PnMExJMbgXyl9yT3R4KpY,5473
56
+ relay_server/docs/concepts/security.md,sha256=T8QPVd4OK9r_uRqwDj4jHOB7kasJa4DttMcJt8nRcpU,3413
57
+ relay_server/docs/concepts/tasks.md,sha256=HFI0NT7zdMPyOD3edbNOx7ZGMF2Y_El_jgrB6bWOoms,5103
58
+ relay_server/docs/concepts/tokens.md,sha256=7V1eBtWl01G8yLCTLJ4hOviKPtbYJrn_Zj9nrp6if6I,3004
59
+ relay_server/docs/federation/concept.md,sha256=BDi_hL5_zmZT9ec4nkeX7Kt-7jnPKqQQlTEEc4tySYE,2979
60
+ relay_server/docs/flow/runner.md,sha256=pFuArVn4c0-AaubkmYqyNyBr3bDtBZSXM5B6enPJA1w,4369
61
+ relay_server/docs/node/capabilities.md,sha256=FX3kqVpwfxp-kdXa7QoykQW0NMskeYmRs8uz1HbTpRE,7589
62
+ relay_server/docs/node/cli.md,sha256=HrwuUCAzYA60-ld516egAv0DR1GeYqJL_0gBTZmdJZc,4740
63
+ relay_server/docs/node/config.md,sha256=qyv_SuMxUW0FjqV1xyiLvqqjHpfLOc9aS6qlLaZvoLI,4611
64
+ relay_server/docs/node/operations.md,sha256=g0oEgs1OlVzVCZF5BixUUvVU3v78qmnLOmWnSvLKjtM,4184
65
+ relay_server/docs/node/setup.md,sha256=V6bcBoYRLM8tcktE9KY-hN0f1UlcEXaRBLFDd9RsVak,6506
66
+ relay_server/docs/node/tokens.md,sha256=xtkn8Q3CTR1CFitsc8nASX9du_0eOFGv70lCCkcLe_8,3160
67
+ relay_server/docs/node/handlers/contract.md,sha256=JmKnbVWTH_OyRndKjn4I_IbYtjLvFvZCsGxK5vTn9co,6165
68
+ relay_server/docs/node/handlers/primitives.md,sha256=TMBFXHV25i7fYOOslC99bVa2LCHf07AaY2v0wlEBRE8,5883
69
+ relay_server/docs/node/integrations/hermes.md,sha256=tI6cFrMN7NXcR5yRUALmkZ5SB-q_cxUrYULTTojNESE,6617
70
+ relay_server/docs/node/integrations/home-assistant.md,sha256=nW1P7HlyEVhI0xoA5MGF86VimYERaF3Y2YiIjArQpZA,8811
71
+ relay_server/docs/reference/api.md,sha256=Fk0ZS9GZHC1t9HH2xyWkk5Wh_wrhJz7KVYL3UztPhYc,19531
72
+ relay_server/docs/reference/database-backends.md,sha256=GUVwN3wuEca2Bthw_mmqWZg0rkFMh42yM8bl31j3I4I,4550
73
+ relay_server/docs/reference/design-board.md,sha256=Zm36WJ9tU63yggOzUC9lp0ikQO0UbKWy-YW0Ykfkb70,1829
74
+ relay_server/docs/server/admin.md,sha256=a5ACcEWWc0c8kKiTbAXLIow1P8KiLKXeJFVGshYQ46o,3895
75
+ relay_server/docs/server/dashboard.md,sha256=--VsQxIgY3zct5Ow8_OeV-YPnMR7EYIkXUP3KgCd92M,4446
76
+ relay_server/docs/server/database.md,sha256=WftrCxnyglL-E22L11NhffsQ8d6U8z7rVI-kVkmefag,2751
77
+ relay_server/docs/server/docker.md,sha256=lzTkiCYteOPhsGdsDTyl9c0DV5BK55PyxXyREQqH7TI,4680
78
+ relay_server/docs/server/setup.md,sha256=Xb884sWBS6AltZTF4b2dAVvpPFQsN4qAkt0X2ucqkXI,8828
79
+ relay_server/docs/storage/qnap.md,sha256=nM9AyW99Q3QWHOjzyWjE3F0VMzZtj5PDJpPT6IylS0g,3619
80
+ relay_server/docs/storage/storage.md,sha256=iFjQaoSAudFZKRFfF5h91emBYy5PFQaJUVqEgJCslqU,6608
81
+ relay_server/models/__init__.py,sha256=RnV2yNyVPrHXvdGyVE1kC2jDUhiE4WPCVw4eRfH5lYQ,16843
82
+ relay_server/models/capability.py,sha256=rUveWJAmmqWauAQMSjzSgZpW7anbKncW6a0lUee5X30,4936
83
+ relay_server/models/discovery.py,sha256=w6sQf2fylHmfAunyV1K-9_Sp3R6n_xwtJlVhTmjupBM,2372
84
+ relay_server/models/task.py,sha256=lgEK74v1j0Q_rie8iJR-TXbseTs-5QH_jwCG0kGS-8Q,6077
85
+ relay_server/static/admin.css,sha256=4PYdxBbEClkJ3dvJb_USoOhpYiL3_tfNhLrLlI3MH-I,17329
86
+ relay_server/static/admin.html,sha256=J8lZEFSuv5brN2_5teXIKJRvfbtZyrjRhyJ59d0RRQ8,9504
87
+ relay_server/static/admin.js,sha256=EDhNco6rbePqqCRXKot2tTMfKqF0sZnKwIBZDOj2D_M,31639
88
+ relay_server/static/bootstrap.html,sha256=uVirYWB427yN5mDB1DR2dqiCEUg8Ur7j2h6EPOSFQT0,2697
89
+ relay_server/static/bootstrap.js,sha256=g4DxT_YHVCubrv6QfHxSKx_5Gnlmae8zxYJp5YhX2xo,3654
90
+ relay_server/static/change-password.html,sha256=_57L8uPgRXhl8X6u_6usLelnywKcnyeqVPvtZkmQkN0,2307
91
+ relay_server/static/change-password.js,sha256=Cvk1vNVLc55r1c3PkGILwgyhHQ7qmyiOdqDcMSelP8g,2155
92
+ relay_server/static/cluster.css,sha256=yZK2-pe5yd6PN2nX7vY0_M_eZ0yvFtMSd0KR5jopx60,18391
93
+ relay_server/static/dashboard.html,sha256=J_DuzwsLi75ls8yi9ZNJAilZH2ceTZEv3hAXjE8HtBQ,3544
94
+ relay_server/static/dashboard.js,sha256=oatrfNEo73wtGp_Up-1E7LchBJaCotDbTWTpuJjqAFE,787
95
+ relay_server/static/login.html,sha256=plfZHj0XsUT3K7l5RHxRj3DLG2QHQPC5rrbrLSw0Lcc,3500
96
+ relay_server/static/login.js,sha256=gbl34W-pKilYsjLRq1cedxREIdW8HD9TMUJoGs-8soA,822
97
+ relay_server/static/metrics.html,sha256=V3fALtBFYOdG8vyTDN-5F93_gfZur06oANOqYmYHM8I,4511
98
+ relay_server/static/metrics.js,sha256=cJLByF3zzqXXaEO1R334fNrlioIGm-JkvBUg3i-E_KQ,7464
99
+ relay_server/static/node-profile.html,sha256=tXvvNt8nEucAtEL6rBO_iMjDyH41PF1xpZLjTrmjRc4,1669
100
+ relay_server/static/node-profile.js,sha256=DLE5Lnyddkq5FTu1V0tNdjWiK6x3FmuxnAvQjpTXZ6U,9116
101
+ relay_server/static/user-profile.html,sha256=1S4pGEj-eGomjkBc7UcvWLpfHDKDLCzVfcEQJVyBVeg,1774
102
+ relay_server/static/user-profile.js,sha256=FDzkFuFVt-lPWK09gh5h5ImqFQbf3ofKaMQJzUJ6yn8,5003
103
+ relay_server/utils/__init__.py,sha256=qHkwUQnAipyD7orncfDbJCaQ47mc02h90FgWy9ad_2o,25
104
+ iowap_server-2.0.0.dist-info/METADATA,sha256=VWbLUGwMdd9f8u_SwSY6TTTwzQnen5guJl2zsZKPH1M,5658
105
+ iowap_server-2.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
106
+ iowap_server-2.0.0.dist-info/entry_points.txt,sha256=536srCRdIISldb77GnWGY_S8R2SpiOREmbeSOQwuNC8,95
107
+ iowap_server-2.0.0.dist-info/top_level.txt,sha256=SL2AwoTq5YPH36IduXs42cTGL0ZEtz6h0Y_SjYcUEgU,13
108
+ iowap_server-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ relay-recovery = relay_server.cli:main
3
+ relay-server = relay_server.main:main
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2026 iowap-org
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
+ relay_server
relay_server/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # iowap-server
2
+
3
+ **IOWAP Relay Server — API, Scheduler, Auth, Dashboard, Metrics**
4
+
5
+ The relay server is the central coordinator in the IOWAP ecosystem. Nodes register their capabilities via heartbeat, and the relay matches tasks to capable nodes. It is a **purpose-agnostic scheduler** — it knows nothing about the work itself, only who can do what.
6
+
7
+ ## Features
8
+
9
+ - **Capability Registry** — nodes heartbeat their capabilities, relay maintains a live registry
10
+ - **Task Scheduler** — match tasks to capable nodes, claim/release lifecycle with retry
11
+ - **Auth & Security** — token-based node auth, CSRF-protected dashboard, TLS support
12
+ - **Dashboard** — web UI for node overview, task monitoring, capability browsing, metrics
13
+ - **SSE Events** — real-time event stream for connected nodes
14
+ - **Status System** — node/task/stage status registry with valid transitions
15
+ - **Event Bus** — publish/subscribe internal events for monitoring & extensibility
16
+ - **PostgreSQL + SQLite** — production (Postgres) and dev (SQLite) backends
17
+ - **Docker** — `docker compose up` for one-command deployment
18
+ - **Observability** — `/metrics` (Prometheus), `/ready` health checks, structured JSON logs
19
+
20
+ ## Quick Start (Linux)
21
+
22
+ ```bash
23
+ curl -fsSL https://raw.githubusercontent.com/iowap-org/iowap-server/main/install.sh | bash
24
+ ```
25
+
26
+ The installer asks **native (systemd)** vs. **Docker (GHCR image)**, configures the
27
+ database (SQLite default, PostgreSQL local/remote) and environment (homelab HTTP /
28
+ internet TLS), creates the master admin seed, and prints the dashboard login URL.
29
+
30
+ **Docker (manual)** — pre-built image on GHCR:
31
+
32
+ ```bash
33
+ docker volume create relay-data
34
+ docker run -d --name iowap-server --restart unless-stopped \
35
+ -p 8788:8788 \
36
+ -e RELAY_MASTER_SEED="$(openssl rand -hex 24)" \
37
+ -e RELAY_SESSION_SECRET="$(openssl rand -base64 32)" \
38
+ -e RELAY_SESSION_COOKIE_SECURE=false \
39
+ -v relay-data:/app/.relay \
40
+ ghcr.io/iowap-org/iowap-server:latest
41
+ ```
42
+
43
+ Compose files live in [iowap-org/iowap-docker](https://github.com/iowap-org/iowap-docker) (`server/docker-compose.yml`).
44
+
45
+ **Manual (source)**:
46
+
47
+ ```bash
48
+ git clone --recursive https://github.com/iowap-org/iowap-server.git
49
+ cd iowap-server
50
+ pip install -e . # or: pip install -e ".[postgres]"
51
+ relay-server --host 0.0.0.0 --port 8788
52
+ ```
53
+
54
+ Server starts on `http://localhost:8788` — the dashboard login is under
55
+ `/relay/v2/dashboard/login` (master seed acts as the bootstrap admin until the
56
+ first human admin exists).
57
+
58
+ ## Configuration
59
+
60
+ | Env | Default | Description |
61
+ |-----|---------|-------------|
62
+ | `RELAY_PORT` | 8788 | Server listen port |
63
+ | `RELAY_DB_TYPE` | sqlite | Database backend (sqlite / postgres) |
64
+ | `RELAY_MASTER_SEED` | — | Deterministic master seed for token generation |
65
+ | `TLS_CERTFILE` | — | TLS cert path (enables HTTPS, disables mDNS) |
66
+ | `POSTGRES_*` | — | PostgreSQL connection (when `RELAY_DB_TYPE=postgres`) |
67
+
68
+ See [iowap-docs → server/setup.md](https://github.com/iowap-org/iowap-docs/blob/main/server/setup.md) for full reference.
69
+
70
+ ## Architecture
71
+
72
+ ```
73
+ ┌──────────┐ heartbeat + capabilities ┌───────────┐
74
+ │ Nodes │ ◄─────────────────────────► │ Relay │
75
+ │ (any) │ claim + complete tasks │ Server │
76
+ └──────────┘ └─────┬─────┘
77
+ │
78
+ ┌───────┴───────┐
79
+ │ Dashboard │
80
+ │ (HTMX/HTML) │
81
+ └───────────────┘
82
+ ```
83
+
84
+ ## Docs
85
+
86
+ Full documentation in [iowap-org/iowap-docs](https://github.com/iowap-org/iowap-docs):
87
+
88
+ - [getting-started.md](https://github.com/iowap-org/iowap-docs/blob/main/getting-started.md) — first steps
89
+ - [server/setup.md](https://github.com/iowap-org/iowap-docs/blob/main/server/setup.md) — full setup guide
90
+ - [server/docker.md](https://github.com/iowap-org/iowap-docs/blob/main/server/docker.md) — Docker deployment
91
+ - [reference/api.md](https://github.com/iowap-org/iowap-docs/blob/main/reference/api.md) — API reference
92
+ - [server/dashboard.md](https://github.com/iowap-org/iowap-docs/blob/main/server/dashboard.md) — dashboard usage
93
+ - [server/admin.md](https://github.com/iowap-org/iowap-docs/blob/main/server/admin.md) — administration
94
+
95
+ ## License
96
+
97
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,3 @@
1
+ """IOWAP v2.0 — Distributed agent cluster server."""
2
+
3
+ __version__ = "2.0.0"
@@ -0,0 +1 @@
1
+ """API routers — flat structure, no versioning."""
@@ -0,0 +1,26 @@
1
+ """API v2 routers — only core services."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from relay_server.api.v2.admin import router as admin_router
6
+ from relay_server.api.v2.auth import router as auth_router
7
+ from relay_server.api.v2.cluster import router as cluster_router
8
+ from relay_server.api.v2.dashboard import router as dashboard_router
9
+ from relay_server.api.v2.discovery import router as discovery_router
10
+ from relay_server.api.v2.docs import router as docs_router
11
+ from relay_server.api.v2.events import router as events_router
12
+ from relay_server.api.v2.presence import router as presence_router
13
+ from relay_server.api.v2.scheduler import router as scheduler_router
14
+ from relay_server.api.v2.storage import router as storage_router
15
+
16
+ router = APIRouter()
17
+ router.include_router(docs_router, prefix="/docs", tags=["docs"])
18
+ router.include_router(auth_router, prefix="/auth", tags=["auth"])
19
+ router.include_router(admin_router, prefix="/admin", tags=["admin"])
20
+ router.include_router(discovery_router, prefix="/discovery", tags=["discovery"])
21
+ router.include_router(scheduler_router, prefix="/scheduler", tags=["scheduler"])
22
+ router.include_router(presence_router, prefix="/presence", tags=["presence"])
23
+ router.include_router(events_router, prefix="/events", tags=["events"])
24
+ router.include_router(dashboard_router, prefix="/dashboard", tags=["dashboard"])
25
+ router.include_router(cluster_router, prefix="/cluster", tags=["cluster"])
26
+ router.include_router(storage_router, prefix="/storage", tags=["storage"])
@@ -0,0 +1,236 @@
1
+ """Administration router for node approval and cluster management."""
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, status
6
+
7
+ from relay_server.api.v2.security import (
8
+ check_dashboard_permission,
9
+ require_admin_or_dashboard_user,
10
+ require_dashboard_user,
11
+ )
12
+ from relay_server.core.auth import approve_node
13
+ from relay_server.core.db import log_audit_event, q
14
+ from relay_server.models import AuthContext, NodeApproval, TokenResponse
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ @router.get("/nodes")
20
+ async def admin_list_nodes(ctx: AuthContext = Depends(require_admin_or_dashboard_user)):
21
+ check_dashboard_permission(ctx, "dashboard:view")
22
+ from relay_server.core.db import get_conn
23
+
24
+ conn = get_conn()
25
+ try:
26
+ rows = conn.execute(
27
+ q("SELECT node_id, node_name, endpoint, capabilities, status, role, last_seen "
28
+ "FROM nodes WHERE node_id != ? ORDER BY registered_at DESC", ("__dashboard_admin__",)),
29
+ ).fetchall()
30
+ return {
31
+ "nodes": [
32
+ {
33
+ "node_id": r["node_id"],
34
+ "node_name": r["node_name"],
35
+ "endpoint": r["endpoint"],
36
+ "capabilities": _parse_caps(r["capabilities"]),
37
+ "status": r["status"],
38
+ "role": r["role"],
39
+ "last_seen": r["last_seen"],
40
+ }
41
+ for r in rows
42
+ ]
43
+ }
44
+ finally:
45
+ conn.close()
46
+
47
+
48
+ @router.post("/nodes/{node_id}/approve", response_model=TokenResponse)
49
+ async def admin_approve_node(
50
+ node_id: str,
51
+ body: NodeApproval,
52
+ ctx: AuthContext = Depends(require_admin_or_dashboard_user),
53
+ ):
54
+ check_dashboard_permission(ctx, "nodes:approve")
55
+ caps = [c.model_dump() for c in body.capabilities] if body.capabilities else None
56
+ token = approve_node(
57
+ node_id=node_id,
58
+ role=body.role,
59
+ capabilities=caps,
60
+ endpoint=body.endpoint,
61
+ )
62
+ if not token:
63
+ raise HTTPException(
64
+ status_code=status.HTTP_404_NOT_FOUND,
65
+ detail="Pending node not found or already approved",
66
+ )
67
+ log_audit_event(
68
+ actor_id=ctx.node_id,
69
+ actor_name=ctx.node_name,
70
+ action="node.approve",
71
+ resource_type="node",
72
+ resource_id=node_id,
73
+ details=f"role={body.role}",
74
+ )
75
+ return _build_token_response(token)
76
+
77
+
78
+ @router.post("/nodes/{node_id}/token", response_model=TokenResponse)
79
+ async def admin_issue_node_token(
80
+ node_id: str,
81
+ ctx: AuthContext = Depends(require_admin_or_dashboard_user),
82
+ ):
83
+ """Issue a new runtime token for an already approved node."""
84
+ check_dashboard_permission(ctx, "nodes:token")
85
+ from relay_server.config import settings
86
+ from relay_server.core.auth import _create_token
87
+ from relay_server.core.db import get_conn
88
+
89
+ conn = get_conn()
90
+ try:
91
+ row = conn.execute(
92
+ q("SELECT node_id, node_name, role FROM nodes WHERE node_id = ? AND status IN (?, ?)", (node_id, "approved", "offline")),
93
+ ).fetchone()
94
+ if not row:
95
+ raise HTTPException(
96
+ status_code=status.HTTP_404_NOT_FOUND,
97
+ detail="Approved/offline node not found",
98
+ )
99
+ # Invalidate old runtime tokens for this node to avoid ambiguity.
100
+ conn.execute(
101
+ q("DELETE FROM node_tokens WHERE node_id = ? AND token_type = ?", (node_id, "runtime")),
102
+ )
103
+ conn.commit()
104
+ token = _create_token(
105
+ node_id=row["node_id"],
106
+ node_name=row["node_name"],
107
+ role=row["role"],
108
+ token_type="runtime",
109
+ pending=False,
110
+ ttl_hours=settings.token_ttl_hours,
111
+ )
112
+ finally:
113
+ conn.close()
114
+
115
+ log_audit_event(
116
+ actor_id=ctx.node_id,
117
+ actor_name=ctx.node_name,
118
+ action="node.issue_token",
119
+ resource_type="node",
120
+ resource_id=node_id,
121
+ )
122
+ return _build_token_response(token)
123
+
124
+
125
+ @router.delete("/nodes/{node_id}")
126
+ async def admin_delete_node(
127
+ node_id: str,
128
+ ctx: AuthContext = Depends(require_admin_or_dashboard_user),
129
+ ):
130
+ """Delete a node and all its associated tokens and presence data."""
131
+ check_dashboard_permission(ctx, "nodes:delete")
132
+ from relay_server.core.db import get_conn
133
+
134
+ conn = get_conn()
135
+ try:
136
+ row = conn.execute(
137
+ q("SELECT node_id FROM nodes WHERE node_id = ?", (node_id,)),
138
+ ).fetchone()
139
+ if not row:
140
+ raise HTTPException(
141
+ status_code=status.HTTP_404_NOT_FOUND,
142
+ detail="Node not found",
143
+ )
144
+
145
+ # Clean up dependent records before removing the node itself
146
+ # (foreign keys are enabled, so deleting in the right order is required).
147
+ conn.execute(q("DELETE FROM node_tokens WHERE node_id = ?", (node_id,)))
148
+ conn.execute(q("DELETE FROM presence WHERE node_id = ?", (node_id,)))
149
+ conn.execute(
150
+ q("UPDATE tasks SET owner_node_id = NULL WHERE owner_node_id = ?", (node_id,)),
151
+ )
152
+ # F-19: fail (not orphan) the deleted node's claimed stages + their
153
+ # tasks. The helper clears the claim fields on the failed rows and
154
+ # publishes task-level events inline; the stage-level events follow
155
+ # after commit below (D-2).
156
+ from relay_server.core.scheduler import fail_stages_of_deleted_node
157
+
158
+ now = datetime.now(UTC).isoformat()
159
+ failed_stage_ids = fail_stages_of_deleted_node(conn, node_id, now)
160
+ # D-13 (F-REV-1): terminal stages keep their claim columns as
161
+ # completion history (complete_stage never clears them), so the
162
+ # claimed-only helper above leaves those FK rows dangling. Clear the
163
+ # remaining node references WITHOUT touching any status — exactly
164
+ # what main's status-agnostic NULL-claim UPDATE did for these rows.
165
+ conn.execute(
166
+ q("UPDATE task_stages SET claimed_by = NULL, claimed_at = NULL, "
167
+ "claim_expires_at = NULL WHERE claimed_by = ? AND status != 'claimed'",
168
+ (node_id,)),
169
+ )
170
+ conn.execute(
171
+ q("UPDATE artifacts SET created_by = NULL WHERE created_by = ?", (node_id,)),
172
+ )
173
+ conn.execute(q("DELETE FROM nodes WHERE node_id = ?", (node_id,)))
174
+ conn.commit()
175
+ # Events AFTER commit so consumers see the final rows.
176
+ from relay_server.core.events import event_bus
177
+
178
+ for stage_id in failed_stage_ids:
179
+ event_bus.publish_sync(
180
+ "stage_failed", {"stage_id": stage_id, "reason": "node_deleted"}
181
+ )
182
+ event_bus.publish_sync(
183
+ "status_changed",
184
+ {
185
+ "entity_type": "stage",
186
+ "entity_id": stage_id,
187
+ "old_status": "claimed",
188
+ "new_status": "failed",
189
+ },
190
+ )
191
+ finally:
192
+ conn.close()
193
+
194
+ log_audit_event(
195
+ actor_id=ctx.node_id,
196
+ actor_name=ctx.node_name,
197
+ action="node.delete",
198
+ resource_type="node",
199
+ resource_id=node_id,
200
+ )
201
+ return {"deleted": True, "node_id": node_id}
202
+
203
+
204
+ def _build_token_response(token: str) -> TokenResponse:
205
+ info = validate_token_safe(token)
206
+ return TokenResponse(
207
+ node_id=info["node_id"],
208
+ node_name=info["node_name"],
209
+ status=info["status"],
210
+ token_type="runtime",
211
+ token=token,
212
+ expires_at=info.get("expires_at", ""),
213
+ )
214
+
215
+
216
+ def validate_token_safe(token: str):
217
+ from relay_server.core.auth import validate_token
218
+
219
+ info = validate_token(token, require_approved=False)
220
+ if not info:
221
+ raise HTTPException(
222
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
223
+ detail="Token created but could not be validated",
224
+ )
225
+ return info
226
+
227
+
228
+ def _parse_caps(value):
229
+ import json
230
+
231
+ if not value:
232
+ return []
233
+ try:
234
+ return json.loads(value)
235
+ except Exception:
236
+ return []