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.
- iowap_server-2.0.0.dist-info/METADATA +129 -0
- iowap_server-2.0.0.dist-info/RECORD +108 -0
- iowap_server-2.0.0.dist-info/WHEEL +5 -0
- iowap_server-2.0.0.dist-info/entry_points.txt +3 -0
- iowap_server-2.0.0.dist-info/licenses/LICENSE +21 -0
- iowap_server-2.0.0.dist-info/top_level.txt +1 -0
- relay_server/README.md +97 -0
- relay_server/__init__.py +3 -0
- relay_server/api/__init__.py +1 -0
- relay_server/api/v2/__init__.py +26 -0
- relay_server/api/v2/admin.py +236 -0
- relay_server/api/v2/auth.py +402 -0
- relay_server/api/v2/cluster.py +485 -0
- relay_server/api/v2/dashboard.py +1181 -0
- relay_server/api/v2/discovery.py +177 -0
- relay_server/api/v2/docs.py +197 -0
- relay_server/api/v2/events.py +63 -0
- relay_server/api/v2/presence.py +50 -0
- relay_server/api/v2/scheduler.py +276 -0
- relay_server/api/v2/security.py +249 -0
- relay_server/api/v2/storage.py +307 -0
- relay_server/cli.py +95 -0
- relay_server/config.py +234 -0
- relay_server/core/__init__.py +1 -0
- relay_server/core/artifacts.py +388 -0
- relay_server/core/auth.py +654 -0
- relay_server/core/chunked_upload.py +252 -0
- relay_server/core/db.py +1605 -0
- relay_server/core/db_postgres.py +80 -0
- relay_server/core/discovery.py +828 -0
- relay_server/core/events.py +178 -0
- relay_server/core/logging_setup.py +40 -0
- relay_server/core/maintenance.py +338 -0
- relay_server/core/metrics.py +351 -0
- relay_server/core/net_guard.py +238 -0
- relay_server/core/node_registry.py +235 -0
- relay_server/core/presence.py +180 -0
- relay_server/core/route_registry.py +506 -0
- relay_server/core/scheduler.py +1329 -0
- relay_server/core/session.py +79 -0
- relay_server/core/status.py +291 -0
- relay_server/core/tables.py +325 -0
- relay_server/core/users.py +422 -0
- relay_server/core/zeroconf.py +166 -0
- relay_server/docs/CONTRIBUTING.md +76 -0
- relay_server/docs/README.md +96 -0
- relay_server/docs/captures/README.md +43 -0
- relay_server/docs/captures/capabilities-20261016.md +1128 -0
- relay_server/docs/captures/health-20261016.md +14 -0
- relay_server/docs/captures/nodes-20261016.md +71 -0
- relay_server/docs/clients/mcp.md +86 -0
- relay_server/docs/concepts/artifacts.md +59 -0
- relay_server/docs/concepts/capabilities.md +132 -0
- relay_server/docs/concepts/flow.md +107 -0
- relay_server/docs/concepts/glossary.md +41 -0
- relay_server/docs/concepts/nodes.md +114 -0
- relay_server/docs/concepts/observability.md +71 -0
- relay_server/docs/concepts/overview.md +93 -0
- relay_server/docs/concepts/security.md +71 -0
- relay_server/docs/concepts/tasks.md +112 -0
- relay_server/docs/concepts/tokens.md +67 -0
- relay_server/docs/federation/concept.md +60 -0
- relay_server/docs/flow/runner.md +112 -0
- relay_server/docs/getting-started.md +94 -0
- relay_server/docs/node/capabilities.md +204 -0
- relay_server/docs/node/cli.md +86 -0
- relay_server/docs/node/config.md +109 -0
- relay_server/docs/node/handlers/contract.md +132 -0
- relay_server/docs/node/handlers/primitives.md +140 -0
- relay_server/docs/node/integrations/hermes.md +146 -0
- relay_server/docs/node/integrations/home-assistant.md +180 -0
- relay_server/docs/node/operations.md +100 -0
- relay_server/docs/node/setup.md +200 -0
- relay_server/docs/node/tokens.md +90 -0
- relay_server/docs/reference/api.md +382 -0
- relay_server/docs/reference/database-backends.md +129 -0
- relay_server/docs/reference/design-board.md +37 -0
- relay_server/docs/server/admin.md +114 -0
- relay_server/docs/server/dashboard.md +109 -0
- relay_server/docs/server/database.md +79 -0
- relay_server/docs/server/docker.md +127 -0
- relay_server/docs/server/setup.md +261 -0
- relay_server/docs/storage/qnap.md +95 -0
- relay_server/docs/storage/storage.md +120 -0
- relay_server/main.py +462 -0
- relay_server/models/__init__.py +488 -0
- relay_server/models/capability.py +131 -0
- relay_server/models/discovery.py +75 -0
- relay_server/models/task.py +191 -0
- relay_server/static/admin.css +562 -0
- relay_server/static/admin.html +238 -0
- relay_server/static/admin.js +812 -0
- relay_server/static/bootstrap.html +54 -0
- relay_server/static/bootstrap.js +93 -0
- relay_server/static/change-password.html +43 -0
- relay_server/static/change-password.js +47 -0
- relay_server/static/cluster.css +663 -0
- relay_server/static/dashboard.html +77 -0
- relay_server/static/dashboard.js +21 -0
- relay_server/static/login.html +64 -0
- relay_server/static/login.js +20 -0
- relay_server/static/metrics.html +98 -0
- relay_server/static/metrics.js +204 -0
- relay_server/static/node-profile.html +62 -0
- relay_server/static/node-profile.js +217 -0
- relay_server/static/user-profile.html +59 -0
- relay_server/static/user-profile.js +125 -0
- 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,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).
|
relay_server/__init__.py
ADDED
|
@@ -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 []
|