@agentunion/kite 1.0.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/event_hub/entry.py +305 -26
- package/core/event_hub/hub.py +8 -0
- package/core/event_hub/server.py +80 -17
- package/core/kite_log.py +241 -0
- package/core/launcher/entry.py +978 -284
- package/core/launcher/process_manager.py +456 -46
- package/core/registry/entry.py +272 -3
- package/core/registry/server.py +339 -289
- package/core/registry/store.py +10 -4
- package/extensions/agents/__init__.py +1 -0
- package/extensions/agents/assistant/__init__.py +1 -0
- package/extensions/agents/assistant/entry.py +380 -0
- package/extensions/agents/assistant/module.md +22 -0
- package/extensions/agents/assistant/server.py +236 -0
- package/extensions/channels/__init__.py +1 -0
- package/extensions/channels/acp_channel/__init__.py +1 -0
- package/extensions/channels/acp_channel/entry.py +380 -0
- package/extensions/channels/acp_channel/module.md +22 -0
- package/extensions/channels/acp_channel/server.py +236 -0
- package/extensions/event_hub_bench/entry.py +664 -379
- package/extensions/event_hub_bench/module.md +2 -1
- package/extensions/services/backup/__init__.py +1 -0
- package/extensions/services/backup/entry.py +380 -0
- package/extensions/services/backup/module.md +22 -0
- package/extensions/services/backup/server.py +244 -0
- package/extensions/services/model_service/__init__.py +1 -0
- package/extensions/services/model_service/entry.py +380 -0
- package/extensions/services/model_service/module.md +22 -0
- package/extensions/services/model_service/server.py +236 -0
- package/extensions/services/watchdog/entry.py +460 -147
- package/extensions/services/watchdog/module.md +3 -0
- package/extensions/services/watchdog/monitor.py +128 -13
- package/extensions/services/watchdog/server.py +75 -13
- package/extensions/services/web/__init__.py +1 -0
- package/extensions/services/web/config.yaml +149 -0
- package/extensions/services/web/entry.py +487 -0
- package/extensions/services/web/module.md +24 -0
- package/extensions/services/web/routes/__init__.py +1 -0
- package/extensions/services/web/routes/routes_call.py +189 -0
- package/extensions/services/web/routes/routes_config.py +512 -0
- package/extensions/services/web/routes/routes_contacts.py +98 -0
- package/extensions/services/web/routes/routes_devlog.py +99 -0
- package/extensions/services/web/routes/routes_phone.py +81 -0
- package/extensions/services/web/routes/routes_sms.py +48 -0
- package/extensions/services/web/routes/routes_stats.py +17 -0
- package/extensions/services/web/routes/routes_voicechat.py +554 -0
- package/extensions/services/web/routes/schemas.py +216 -0
- package/extensions/services/web/server.py +332 -0
- package/extensions/services/web/static/css/style.css +1064 -0
- package/extensions/services/web/static/index.html +1445 -0
- package/extensions/services/web/static/js/app.js +4671 -0
- package/extensions/services/web/vendor/__init__.py +1 -0
- package/extensions/services/web/vendor/bluetooth/__init__.py +0 -0
- package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
- package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
- package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
- package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
- package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
- package/extensions/services/web/vendor/config.py +139 -0
- package/extensions/services/web/vendor/conversation/__init__.py +0 -0
- package/extensions/services/web/vendor/conversation/asr.py +936 -0
- package/extensions/services/web/vendor/conversation/engine.py +548 -0
- package/extensions/services/web/vendor/conversation/llm.py +534 -0
- package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
- package/extensions/services/web/vendor/conversation/tts.py +322 -0
- package/extensions/services/web/vendor/conversation/vad.py +138 -0
- package/extensions/services/web/vendor/storage/__init__.py +1 -0
- package/extensions/services/web/vendor/storage/identity.py +312 -0
- package/extensions/services/web/vendor/storage/store.py +507 -0
- package/extensions/services/web/vendor/task/__init__.py +0 -0
- package/extensions/services/web/vendor/task/manager.py +864 -0
- package/extensions/services/web/vendor/task/models.py +45 -0
- package/extensions/services/web/vendor/task/webhook.py +263 -0
- package/extensions/services/web/vendor/tools/__init__.py +0 -0
- package/extensions/services/web/vendor/tools/registry.py +321 -0
- package/main.py +230 -90
- package/package.json +1 -1
package/core/registry/server.py
CHANGED
|
@@ -1,289 +1,339 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Registry HTTP server.
|
|
3
|
-
7 endpoints: /modules, /lookup, /get/{path}, /tokens, /verify, /query, /health.
|
|
4
|
-
All endpoints except /health require Bearer token auth.
|
|
5
|
-
|
|
6
|
-
Delayed ready mechanism (mechanism 7):
|
|
7
|
-
Registry does NOT send module.ready immediately after HTTP starts.
|
|
8
|
-
When Event Hub registers (POST /modules with metadata.ws_endpoint),
|
|
9
|
-
Registry connects to Event Hub WS and then sends module.ready.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import asyncio
|
|
13
|
-
import json
|
|
14
|
-
import uuid
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
import websockets
|
|
18
|
-
from fastapi import FastAPI, Request, HTTPException
|
|
19
|
-
from fastapi.responses import JSONResponse
|
|
20
|
-
|
|
21
|
-
from .store import RegistryStore
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class RegistryServer:
|
|
25
|
-
"""FastAPI-based Registry HTTP server."""
|
|
26
|
-
|
|
27
|
-
def __init__(self, store: RegistryStore, launcher_token: str = "", advertise_ip: str = "127.0.0.1"):
|
|
28
|
-
self.store = store
|
|
29
|
-
self.launcher_token = launcher_token
|
|
30
|
-
self.advertise_ip = advertise_ip
|
|
31
|
-
self.port: int = 0 # set by entry.py before uvicorn.run
|
|
32
|
-
self.app = self._create_app()
|
|
33
|
-
self._ttl_task: asyncio.Task | None = None
|
|
34
|
-
# Event Hub WebSocket
|
|
35
|
-
self._event_hub_ws_url: str = ""
|
|
36
|
-
self._ws: object | None = None
|
|
37
|
-
self._ws_task: asyncio.Task | None = None
|
|
38
|
-
self._event_hub_connected = False
|
|
39
|
-
self._ready_sent = False
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
module_id
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if not
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1
|
+
"""
|
|
2
|
+
Registry HTTP server.
|
|
3
|
+
7 endpoints: /modules, /lookup, /get/{path}, /tokens, /verify, /query, /health.
|
|
4
|
+
All endpoints except /health require Bearer token auth.
|
|
5
|
+
|
|
6
|
+
Delayed ready mechanism (mechanism 7):
|
|
7
|
+
Registry does NOT send module.ready immediately after HTTP starts.
|
|
8
|
+
When Event Hub registers (POST /modules with metadata.ws_endpoint),
|
|
9
|
+
Registry connects to Event Hub WS and then sends module.ready.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import websockets
|
|
18
|
+
from fastapi import FastAPI, Request, HTTPException
|
|
19
|
+
from fastapi.responses import JSONResponse
|
|
20
|
+
|
|
21
|
+
from .store import RegistryStore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegistryServer:
|
|
25
|
+
"""FastAPI-based Registry HTTP server."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, store: RegistryStore, launcher_token: str = "", advertise_ip: str = "127.0.0.1"):
|
|
28
|
+
self.store = store
|
|
29
|
+
self.launcher_token = launcher_token
|
|
30
|
+
self.advertise_ip = advertise_ip
|
|
31
|
+
self.port: int = 0 # set by entry.py before uvicorn.run
|
|
32
|
+
self.app = self._create_app()
|
|
33
|
+
self._ttl_task: asyncio.Task | None = None
|
|
34
|
+
# Event Hub WebSocket
|
|
35
|
+
self._event_hub_ws_url: str = ""
|
|
36
|
+
self._ws: object | None = None
|
|
37
|
+
self._ws_task: asyncio.Task | None = None
|
|
38
|
+
self._event_hub_connected = False
|
|
39
|
+
self._ready_sent = False
|
|
40
|
+
self._uvicorn_server = None # set by entry.py for graceful shutdown
|
|
41
|
+
self._shutting_down = False
|
|
42
|
+
|
|
43
|
+
def _extract_token(self, request: Request) -> str:
|
|
44
|
+
"""Extract Bearer token from Authorization header."""
|
|
45
|
+
auth = request.headers.get("Authorization", "")
|
|
46
|
+
if auth.startswith("Bearer "):
|
|
47
|
+
return auth[7:].strip()
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
def _require_auth(self, request: Request) -> str:
|
|
51
|
+
"""Verify token, return module_id. Raise 401 on failure."""
|
|
52
|
+
token = self._extract_token(request)
|
|
53
|
+
module_id = self.store.verify_token(token)
|
|
54
|
+
if module_id is None:
|
|
55
|
+
raise HTTPException(status_code=401, detail="Invalid or missing token")
|
|
56
|
+
return module_id
|
|
57
|
+
|
|
58
|
+
def _require_launcher(self, request: Request):
|
|
59
|
+
"""Verify the caller is Launcher. Raise 403 if not."""
|
|
60
|
+
token = self._extract_token(request)
|
|
61
|
+
if not self.store.is_launcher(token):
|
|
62
|
+
raise HTTPException(status_code=403, detail="Only Launcher may call this endpoint")
|
|
63
|
+
|
|
64
|
+
# ── Event Hub connection + delayed ready ──
|
|
65
|
+
|
|
66
|
+
async def _try_connect_event_hub(self):
|
|
67
|
+
"""Event Hub just registered — connect to it and send module.ready."""
|
|
68
|
+
if self._event_hub_connected:
|
|
69
|
+
return
|
|
70
|
+
eh = self.store.modules.get("event_hub")
|
|
71
|
+
if not eh:
|
|
72
|
+
return
|
|
73
|
+
ws_url = (eh.get("metadata") or {}).get("ws_endpoint", "")
|
|
74
|
+
if not ws_url:
|
|
75
|
+
return
|
|
76
|
+
self._event_hub_ws_url = ws_url
|
|
77
|
+
if not self._ws_task:
|
|
78
|
+
self._ws_task = asyncio.create_task(self._ws_loop())
|
|
79
|
+
|
|
80
|
+
async def _ws_loop(self):
|
|
81
|
+
"""Connect to Event Hub, reconnect on failure."""
|
|
82
|
+
retry_delay = 0.5 # start with 0.5s
|
|
83
|
+
max_delay = 30 # cap at 30s
|
|
84
|
+
while not self._shutting_down:
|
|
85
|
+
try:
|
|
86
|
+
await self._ws_connect()
|
|
87
|
+
retry_delay = 0.5 # reset on successful connection
|
|
88
|
+
except asyncio.CancelledError:
|
|
89
|
+
return
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"[registry] Event Hub connection error: {e}, retrying in {retry_delay:.1f}s")
|
|
92
|
+
self._ws = None
|
|
93
|
+
self._event_hub_connected = False
|
|
94
|
+
if self._shutting_down:
|
|
95
|
+
return
|
|
96
|
+
await asyncio.sleep(retry_delay)
|
|
97
|
+
retry_delay = min(retry_delay * 2, max_delay) # exponential backoff
|
|
98
|
+
|
|
99
|
+
async def _ws_connect(self):
|
|
100
|
+
"""Single WebSocket session. On first connect, send module.ready."""
|
|
101
|
+
# Use registry's own per-module token to avoid conflicting with Launcher's connection
|
|
102
|
+
token = self.store.token_map.get("registry", "") or self.launcher_token
|
|
103
|
+
ws_url = f"{self._event_hub_ws_url}?token={token}&id=registry"
|
|
104
|
+
async with websockets.connect(ws_url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
105
|
+
self._ws = ws
|
|
106
|
+
self._event_hub_connected = True
|
|
107
|
+
print("[registry] Connected to Event Hub")
|
|
108
|
+
|
|
109
|
+
# Subscribe to shutdown events
|
|
110
|
+
await ws.send(json.dumps({"type": "subscribe", "events": ["module.shutdown"]}))
|
|
111
|
+
|
|
112
|
+
# Send module.ready on first successful connection (delayed ready mechanism)
|
|
113
|
+
if not self._ready_sent:
|
|
114
|
+
self._ready_sent = True
|
|
115
|
+
# Self-register so watchdog (and others) can discover registry
|
|
116
|
+
# via /lookup. Without this, health checks fail (empty api_endpoint).
|
|
117
|
+
self.store.register_module({
|
|
118
|
+
"module_id": "registry",
|
|
119
|
+
"module_type": "infrastructure",
|
|
120
|
+
"api_endpoint": f"http://{self.advertise_ip}:{self.port}",
|
|
121
|
+
"health_endpoint": "/health",
|
|
122
|
+
})
|
|
123
|
+
await self._send_module_ready()
|
|
124
|
+
|
|
125
|
+
async for raw in ws:
|
|
126
|
+
try:
|
|
127
|
+
msg = json.loads(raw)
|
|
128
|
+
except (json.JSONDecodeError, TypeError):
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
msg_type = msg.get("type", "")
|
|
132
|
+
if msg_type == "event":
|
|
133
|
+
event = msg.get("event", "")
|
|
134
|
+
data = msg.get("data") if isinstance(msg.get("data"), dict) else {}
|
|
135
|
+
if event == "module.shutdown" and data.get("module_id") == "registry":
|
|
136
|
+
await self._handle_shutdown(data)
|
|
137
|
+
return
|
|
138
|
+
elif msg_type == "error":
|
|
139
|
+
print(f"[registry] Event Hub error: {msg.get('message')}")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"[registry] 事件处理异常(已忽略): {e}")
|
|
142
|
+
|
|
143
|
+
async def _send_module_ready(self):
|
|
144
|
+
"""Send module.ready event to Event Hub. Launcher is listening for this."""
|
|
145
|
+
from datetime import datetime, timezone
|
|
146
|
+
msg = {
|
|
147
|
+
"type": "event",
|
|
148
|
+
"event_id": str(uuid.uuid4()),
|
|
149
|
+
"event": "module.ready",
|
|
150
|
+
"source": "registry",
|
|
151
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
152
|
+
"data": {
|
|
153
|
+
"module_id": "registry",
|
|
154
|
+
"api_endpoint": f"http://{self.advertise_ip}:{self.port}",
|
|
155
|
+
"graceful_shutdown": True,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
try:
|
|
159
|
+
await self._ws.send(json.dumps(msg))
|
|
160
|
+
print("[registry] Sent module.ready")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(f"[registry] Failed to send module.ready: {e}")
|
|
163
|
+
|
|
164
|
+
async def _publish_event(self, event_type: str, data: dict):
|
|
165
|
+
"""Publish event to Event Hub. Best-effort, no-op if not connected."""
|
|
166
|
+
if not self._ws:
|
|
167
|
+
return
|
|
168
|
+
from datetime import datetime, timezone
|
|
169
|
+
msg = {
|
|
170
|
+
"type": "event",
|
|
171
|
+
"event_id": str(uuid.uuid4()),
|
|
172
|
+
"event": event_type,
|
|
173
|
+
"source": "registry",
|
|
174
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
175
|
+
"data": data,
|
|
176
|
+
}
|
|
177
|
+
try:
|
|
178
|
+
await self._ws.send(json.dumps(msg))
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
async def _handle_shutdown(self, data: dict):
|
|
183
|
+
"""Handle module.shutdown event — ack, cleanup, ready, exit."""
|
|
184
|
+
print("[registry] Received shutdown request")
|
|
185
|
+
self._shutting_down = True
|
|
186
|
+
# Step 1: Send ack
|
|
187
|
+
await self._publish_event("module.shutdown.ack", {
|
|
188
|
+
"module_id": "registry",
|
|
189
|
+
"estimated_cleanup": 2,
|
|
190
|
+
})
|
|
191
|
+
# Step 2: Cleanup
|
|
192
|
+
if self._ttl_task:
|
|
193
|
+
self._ttl_task.cancel()
|
|
194
|
+
# Step 3: Send ready (before closing WS!)
|
|
195
|
+
await self._publish_event("module.shutdown.ready", {
|
|
196
|
+
"module_id": "registry",
|
|
197
|
+
})
|
|
198
|
+
print("[registry] Shutdown ready, exiting")
|
|
199
|
+
# Step 4: Trigger uvicorn exit (WS will close when uvicorn shuts down)
|
|
200
|
+
if self._uvicorn_server:
|
|
201
|
+
self._uvicorn_server.should_exit = True
|
|
202
|
+
|
|
203
|
+
# ── App factory ──
|
|
204
|
+
|
|
205
|
+
def _create_app(self) -> FastAPI:
|
|
206
|
+
app = FastAPI(title="Kite Registry", docs_url=None, redoc_url=None)
|
|
207
|
+
server = self
|
|
208
|
+
|
|
209
|
+
@app.on_event("startup")
|
|
210
|
+
async def _startup():
|
|
211
|
+
server._ttl_task = asyncio.create_task(server._ttl_loop())
|
|
212
|
+
|
|
213
|
+
@app.on_event("shutdown")
|
|
214
|
+
async def _shutdown():
|
|
215
|
+
if server._ttl_task:
|
|
216
|
+
server._ttl_task.cancel()
|
|
217
|
+
if server._ws_task:
|
|
218
|
+
server._ws_task.cancel()
|
|
219
|
+
if server._ws:
|
|
220
|
+
await server._ws.close()
|
|
221
|
+
|
|
222
|
+
# ── 1. POST /modules ──
|
|
223
|
+
|
|
224
|
+
@app.post("/modules")
|
|
225
|
+
async def modules(request: Request):
|
|
226
|
+
caller = server._require_auth(request)
|
|
227
|
+
body = await request.json()
|
|
228
|
+
action = body.get("action", "")
|
|
229
|
+
|
|
230
|
+
if action == "register":
|
|
231
|
+
if "module_id" not in body:
|
|
232
|
+
raise HTTPException(400, "module_id required")
|
|
233
|
+
# Only Launcher or the module itself may register
|
|
234
|
+
if caller != "launcher" and caller != body["module_id"]:
|
|
235
|
+
raise HTTPException(403, f"Module '{caller}' cannot register as '{body['module_id']}'")
|
|
236
|
+
result = server.store.register_module(body)
|
|
237
|
+
if result.get("ok"):
|
|
238
|
+
mid = body["module_id"]
|
|
239
|
+
await server._publish_event("module.registered", {"module_id": mid})
|
|
240
|
+
# If Event Hub just registered, connect and send module.ready
|
|
241
|
+
ws_endpoint = (body.get("metadata") or {}).get("ws_endpoint")
|
|
242
|
+
if ws_endpoint and mid == "event_hub":
|
|
243
|
+
await server._try_connect_event_hub()
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
elif action == "deregister":
|
|
247
|
+
mid = body.get("module_id")
|
|
248
|
+
if not mid:
|
|
249
|
+
raise HTTPException(400, "module_id required")
|
|
250
|
+
if caller != "launcher" and caller != mid:
|
|
251
|
+
raise HTTPException(403, f"Module '{caller}' cannot deregister '{mid}'")
|
|
252
|
+
result = server.store.deregister_module(mid)
|
|
253
|
+
if result.get("ok"):
|
|
254
|
+
await server._publish_event("module.unregistered", {"module_id": mid})
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
elif action == "heartbeat":
|
|
258
|
+
mid = body.get("module_id")
|
|
259
|
+
if not mid:
|
|
260
|
+
raise HTTPException(400, "module_id required")
|
|
261
|
+
if caller != "launcher" and caller != mid:
|
|
262
|
+
raise HTTPException(403, f"Module '{caller}' cannot heartbeat for '{mid}'")
|
|
263
|
+
result = server.store.heartbeat(mid)
|
|
264
|
+
if result.get("ok"):
|
|
265
|
+
await server._publish_event("module.heartbeat", {"module_id": mid})
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
else:
|
|
269
|
+
raise HTTPException(400, f"Unknown action: {action}")
|
|
270
|
+
|
|
271
|
+
# ── 2. GET /lookup ──
|
|
272
|
+
|
|
273
|
+
@app.get("/lookup")
|
|
274
|
+
async def lookup(request: Request, field: str = None, module: str = None, value: str = None):
|
|
275
|
+
server._require_auth(request)
|
|
276
|
+
return server.store.lookup(field=field, module=module, value=value)
|
|
277
|
+
|
|
278
|
+
# ── 3. GET /get/{path} ──
|
|
279
|
+
|
|
280
|
+
@app.get("/get/{path:path}")
|
|
281
|
+
async def get_by_path(request: Request, path: str):
|
|
282
|
+
server._require_auth(request)
|
|
283
|
+
val, found = server.store.get_by_path(path)
|
|
284
|
+
if not found:
|
|
285
|
+
raise HTTPException(404, f"Path not found: {path}")
|
|
286
|
+
return val
|
|
287
|
+
|
|
288
|
+
# ── 4. POST /tokens ──
|
|
289
|
+
|
|
290
|
+
@app.post("/tokens")
|
|
291
|
+
async def register_tokens(request: Request):
|
|
292
|
+
server._require_launcher(request)
|
|
293
|
+
body = await request.json()
|
|
294
|
+
server.store.register_tokens(body)
|
|
295
|
+
return {"ok": True}
|
|
296
|
+
|
|
297
|
+
# ── 5. POST /verify ──
|
|
298
|
+
|
|
299
|
+
@app.post("/verify")
|
|
300
|
+
async def verify_token(request: Request):
|
|
301
|
+
server._require_auth(request)
|
|
302
|
+
body = await request.json()
|
|
303
|
+
target_token = body.get("token", "")
|
|
304
|
+
module_id = server.store.verify_token(target_token)
|
|
305
|
+
if module_id:
|
|
306
|
+
return {"ok": True, "module_id": module_id}
|
|
307
|
+
return {"ok": False}
|
|
308
|
+
|
|
309
|
+
# ── 6. POST /query (stub) ──
|
|
310
|
+
|
|
311
|
+
@app.post("/query")
|
|
312
|
+
async def query(request: Request):
|
|
313
|
+
server._require_auth(request)
|
|
314
|
+
body = await request.json()
|
|
315
|
+
question = body.get("question", "")
|
|
316
|
+
return {"ok": False, "error": "LLM query not implemented yet", "question": question}
|
|
317
|
+
|
|
318
|
+
# ── 7. GET /health ──
|
|
319
|
+
|
|
320
|
+
@app.get("/health")
|
|
321
|
+
async def health():
|
|
322
|
+
return {
|
|
323
|
+
"status": "healthy",
|
|
324
|
+
"module_count": len(server.store.modules),
|
|
325
|
+
"online_count": sum(
|
|
326
|
+
1 for m in server.store.modules.values()
|
|
327
|
+
if m.get("status") == "online"
|
|
328
|
+
),
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return app
|
|
332
|
+
|
|
333
|
+
async def _ttl_loop(self):
|
|
334
|
+
"""Periodically check heartbeat TTL and publish offline events."""
|
|
335
|
+
while True:
|
|
336
|
+
await asyncio.sleep(10)
|
|
337
|
+
expired = self.store.check_ttl()
|
|
338
|
+
for mid in expired:
|
|
339
|
+
await self._publish_event("module.offline", {"module_id": mid})
|