@agentunion/kite 1.2.0 → 1.3.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/CHANGELOG.md +208 -0
- package/README.md +48 -0
- package/cli.js +1 -1
- package/extensions/agents/assistant/entry.py +30 -81
- package/extensions/agents/assistant/module.md +1 -1
- package/extensions/agents/assistant/server.py +83 -122
- package/extensions/channels/acp_channel/entry.py +30 -81
- package/extensions/channels/acp_channel/module.md +1 -1
- package/extensions/channels/acp_channel/server.py +83 -122
- package/extensions/event_hub_bench/entry.py +81 -121
- package/extensions/services/backup/entry.py +213 -85
- package/extensions/services/model_service/entry.py +213 -85
- package/extensions/services/watchdog/entry.py +513 -460
- package/extensions/services/watchdog/monitor.py +55 -69
- package/extensions/services/web/entry.py +11 -108
- package/extensions/services/web/server.py +120 -77
- package/{core/registry → kernel}/entry.py +65 -37
- package/{core/event_hub/hub.py → kernel/event_hub.py} +61 -81
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +13 -4
- package/kernel/rpc_router.py +388 -0
- package/kernel/server.py +267 -0
- package/launcher/__init__.py +10 -0
- package/launcher/__main__.py +6 -0
- package/launcher/count_lines.py +258 -0
- package/{core/launcher → launcher}/entry.py +693 -767
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/main.py +11 -350
- package/package.json +6 -9
- package/__init__.py +0 -1
- package/__main__.py +0 -15
- package/core/event_hub/BENCHMARK.md +0 -94
- package/core/event_hub/__init__.py +0 -0
- package/core/event_hub/bench.py +0 -459
- package/core/event_hub/bench_extreme.py +0 -308
- package/core/event_hub/bench_perf.py +0 -350
- package/core/event_hub/entry.py +0 -436
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -269
- package/core/kite_log.py +0 -241
- package/core/launcher/__init__.py +0 -0
- package/core/registry/__init__.py +0 -0
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -339
- package/extensions/services/backup/server.py +0 -244
- package/extensions/services/model_service/server.py +0 -236
- package/extensions/services/watchdog/server.py +0 -229
- /package/{core → kernel}/__init__.py +0 -0
- /package/{core/event_hub → kernel}/dedup.py +0 -0
- /package/{core/event_hub → kernel}/router.py +0 -0
- /package/{core/launcher → launcher}/module.md +0 -0
- /package/{core/launcher → launcher}/process_manager.py +0 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
Reads token from stdin
|
|
4
|
-
outputs port via stdout
|
|
2
|
+
Kernel entry point.
|
|
3
|
+
Reads boot_info token + tokens dict from stdin, starts unified WebSocket server,
|
|
4
|
+
outputs port via stdout, self-registers, publishes module.ready.
|
|
5
|
+
|
|
6
|
+
Stdin protocol:
|
|
7
|
+
Line 1: {"token": "launcher_kite_token"} (boot_info)
|
|
8
|
+
Line 2: {"kite": "tokens", "tokens": {"mod1":"tok1", ...}} (token mapping)
|
|
9
|
+
|
|
10
|
+
Stdout protocol:
|
|
11
|
+
{"kite": "port", "port": N} (Kernel port)
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
import builtins
|
|
@@ -19,11 +26,11 @@ import uvicorn
|
|
|
19
26
|
|
|
20
27
|
|
|
21
28
|
# ── Module configuration ──
|
|
22
|
-
MODULE_NAME = "
|
|
29
|
+
MODULE_NAME = "kernel"
|
|
23
30
|
|
|
24
31
|
|
|
25
32
|
def _fmt_elapsed(t0: float) -> str:
|
|
26
|
-
"""Format elapsed time since t0: <1s
|
|
33
|
+
"""Format elapsed time since t0: <1s -> 'NNNms', >=1s -> 'N.Ns', >=10s -> 'NNs'."""
|
|
27
34
|
d = time.monotonic() - t0
|
|
28
35
|
if d < 1:
|
|
29
36
|
return f"{d * 1000:.0f}ms"
|
|
@@ -59,7 +66,6 @@ sys.stderr = _SafeWriter(sys.stderr)
|
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
# ── Timestamped print + log file writer ──
|
|
62
|
-
# Implements format: [timestamp] HH:MM:SS.mmm +delta [module_name] message
|
|
63
69
|
# Independent implementation per module (no shared code dependency)
|
|
64
70
|
|
|
65
71
|
_builtin_print = builtins.print
|
|
@@ -247,8 +253,7 @@ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirnam
|
|
|
247
253
|
if _project_root not in sys.path:
|
|
248
254
|
sys.path.insert(0, _project_root)
|
|
249
255
|
|
|
250
|
-
from
|
|
251
|
-
from core.registry.server import RegistryServer
|
|
256
|
+
from kernel.server import KernelServer
|
|
252
257
|
|
|
253
258
|
|
|
254
259
|
def _read_module_md() -> dict:
|
|
@@ -258,7 +263,6 @@ def _read_module_md() -> dict:
|
|
|
258
263
|
try:
|
|
259
264
|
with open(md_path, "r", encoding="utf-8") as f:
|
|
260
265
|
text = f.read()
|
|
261
|
-
import re
|
|
262
266
|
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
263
267
|
if m:
|
|
264
268
|
try:
|
|
@@ -287,6 +291,39 @@ def _bind_port(preferred: int, host: str) -> int:
|
|
|
287
291
|
return s.getsockname()[1]
|
|
288
292
|
|
|
289
293
|
|
|
294
|
+
def _read_stdin_kite_messages(expected: set[str], timeout: float = 10) -> dict[str, dict]:
|
|
295
|
+
"""Read multiple structured kite messages from stdin until all expected types received.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
expected: set of kite message types to wait for (e.g. {"tokens"})
|
|
299
|
+
timeout: total timeout in seconds
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
dict mapping kite type -> message dict. Missing types are absent from the result.
|
|
303
|
+
"""
|
|
304
|
+
collected: dict[str, dict] = {}
|
|
305
|
+
remaining = set(expected)
|
|
306
|
+
|
|
307
|
+
def _read():
|
|
308
|
+
while remaining:
|
|
309
|
+
try:
|
|
310
|
+
line = sys.stdin.readline().strip()
|
|
311
|
+
if not line:
|
|
312
|
+
return # stdin closed
|
|
313
|
+
msg = json.loads(line)
|
|
314
|
+
if isinstance(msg, dict) and "kite" in msg:
|
|
315
|
+
kite_type = msg["kite"]
|
|
316
|
+
collected[kite_type] = msg
|
|
317
|
+
remaining.discard(kite_type)
|
|
318
|
+
except Exception:
|
|
319
|
+
return # parse error or stdin closed
|
|
320
|
+
|
|
321
|
+
t = threading.Thread(target=_read, daemon=True)
|
|
322
|
+
t.start()
|
|
323
|
+
t.join(timeout=timeout)
|
|
324
|
+
return collected
|
|
325
|
+
|
|
326
|
+
|
|
290
327
|
def main():
|
|
291
328
|
# Initialize log file paths
|
|
292
329
|
global _log_dir, _log_latest_path, _crash_log_path
|
|
@@ -320,54 +357,45 @@ def main():
|
|
|
320
357
|
_t0 = time.monotonic()
|
|
321
358
|
|
|
322
359
|
# Kite environment
|
|
323
|
-
kite_instance = os.environ.get("KITE_INSTANCE", "")
|
|
324
360
|
is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
325
361
|
|
|
326
|
-
# Step 1: Read token from stdin boot_info (only token)
|
|
327
|
-
launcher_token = ""
|
|
328
|
-
try:
|
|
329
|
-
line = sys.stdin.readline().strip()
|
|
330
|
-
if line:
|
|
331
|
-
boot_info = json.loads(line)
|
|
332
|
-
launcher_token = boot_info.get("token", "")
|
|
333
|
-
except Exception:
|
|
334
|
-
pass
|
|
335
|
-
|
|
336
|
-
if not launcher_token:
|
|
337
|
-
print("[registry] 错误: 未通过 stdin boot_info 提供启动器令牌")
|
|
338
|
-
sys.exit(1)
|
|
339
|
-
|
|
340
|
-
print(f"[registry] 已收到启动器令牌 ({len(launcher_token)} 字符) ({_fmt_elapsed(_t0)})")
|
|
341
|
-
|
|
342
362
|
if is_debug:
|
|
343
|
-
print("[
|
|
363
|
+
print("[kernel] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
|
|
344
364
|
|
|
345
|
-
# Step
|
|
365
|
+
# Step 1: Read config from own module.md
|
|
346
366
|
md_config = _read_module_md()
|
|
347
367
|
advertise_ip = md_config["advertise_ip"]
|
|
348
368
|
preferred_port = md_config["preferred_port"]
|
|
349
369
|
|
|
350
|
-
# Step
|
|
351
|
-
|
|
352
|
-
|
|
370
|
+
# Step 2: Generate launcher token
|
|
371
|
+
import secrets
|
|
372
|
+
launcher_token = secrets.token_urlsafe(32)
|
|
373
|
+
|
|
374
|
+
# Step 3: Create KernelServer with launcher_token
|
|
375
|
+
server = KernelServer(launcher_token=launcher_token, advertise_ip=advertise_ip)
|
|
353
376
|
|
|
354
377
|
# Step 4: Bind port
|
|
355
378
|
bind_host = advertise_ip
|
|
356
379
|
port = _bind_port(preferred_port, bind_host)
|
|
380
|
+
server.port = port
|
|
357
381
|
|
|
358
|
-
# Step 5: Output port via stdout
|
|
359
|
-
|
|
360
|
-
print(json.dumps({"kite": "port", "port": port}), flush=True)
|
|
382
|
+
# Step 5: Output port + launcher_token via stdout
|
|
383
|
+
print(json.dumps({"kite": "port", "port": port, "token": launcher_token}), flush=True)
|
|
361
384
|
|
|
362
|
-
print(f"[
|
|
385
|
+
print(f"[kernel] 启动中 {bind_host}:{port} ({_fmt_elapsed(_t0)})")
|
|
363
386
|
|
|
364
|
-
#
|
|
365
|
-
server.
|
|
387
|
+
# Step 6: Self-register in registry (direct memory write)
|
|
388
|
+
server.self_register()
|
|
366
389
|
|
|
390
|
+
# Step 7: Start uvicorn (module.ready will be published when Launcher subscribes)
|
|
367
391
|
try:
|
|
368
392
|
config = uvicorn.Config(server.app, host=bind_host, port=port, log_level="warning")
|
|
369
393
|
uvi_server = uvicorn.Server(config)
|
|
370
394
|
server._uvicorn_server = uvi_server
|
|
395
|
+
|
|
396
|
+
# module.ready is now published when Launcher subscribes (not at startup)
|
|
397
|
+
# This ensures Launcher is connected and subscribed before receiving the event
|
|
398
|
+
|
|
371
399
|
uvi_server.run()
|
|
372
400
|
except Exception as e:
|
|
373
401
|
_write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
Kernel Event Hub logic.
|
|
3
|
+
Connection management, subscription matching, event routing (validate -> dedup -> route),
|
|
4
|
+
per-subscriber delivery queues.
|
|
5
|
+
|
|
6
|
+
Adapted from core/event_hub/hub.py for the merged Kernel module:
|
|
7
|
+
- Removed handle_message, _handle_event, _send_ack, _send_error (moved to RpcRouter)
|
|
8
|
+
- Added publish_event() for RPC-driven event publishing
|
|
9
|
+
- Added publish_internal() for Kernel-originated events (e.g. module.offline)
|
|
10
|
+
- Event delivery uses JSON-RPC 2.0 Notification format
|
|
5
11
|
"""
|
|
6
12
|
|
|
7
13
|
import asyncio
|
|
@@ -14,7 +20,7 @@ try:
|
|
|
14
20
|
except ImportError:
|
|
15
21
|
orjson = None
|
|
16
22
|
|
|
17
|
-
from
|
|
23
|
+
from starlette.websockets import WebSocket
|
|
18
24
|
|
|
19
25
|
from .dedup import EventDedup
|
|
20
26
|
from .router import match_parts
|
|
@@ -31,6 +37,7 @@ def _loads(raw: str):
|
|
|
31
37
|
return orjson.loads(raw)
|
|
32
38
|
return json.loads(raw)
|
|
33
39
|
|
|
40
|
+
|
|
34
41
|
QUEUE_MAXSIZE = 10000
|
|
35
42
|
|
|
36
43
|
|
|
@@ -55,8 +62,6 @@ class EventHub:
|
|
|
55
62
|
self._cnt_dedup = 0
|
|
56
63
|
self._cnt_errors = 0
|
|
57
64
|
self._start_time = time.time()
|
|
58
|
-
# Shutdown callback: set by EventHubServer to handle own shutdown
|
|
59
|
-
self._shutdown_cb = None
|
|
60
65
|
|
|
61
66
|
# ── Connection lifecycle ──
|
|
62
67
|
|
|
@@ -77,14 +82,14 @@ class EventHub:
|
|
|
77
82
|
self._senders[module_id] = asyncio.create_task(
|
|
78
83
|
self._sender_loop(module_id, ws, q)
|
|
79
84
|
)
|
|
80
|
-
print(f"[
|
|
85
|
+
print(f"[kernel] {module_id} connected")
|
|
81
86
|
|
|
82
87
|
async def _close_old(self, module_id: str, ws: WebSocket):
|
|
83
88
|
try:
|
|
84
89
|
await ws.close(code=4000, reason="replaced by new connection")
|
|
85
90
|
except Exception:
|
|
86
91
|
pass
|
|
87
|
-
print(f"[
|
|
92
|
+
print(f"[kernel] Closed old connection for {module_id}")
|
|
88
93
|
|
|
89
94
|
def remove_connection(self, module_id: str):
|
|
90
95
|
"""Clean up on disconnect."""
|
|
@@ -95,7 +100,7 @@ class EventHub:
|
|
|
95
100
|
task = self._senders.pop(module_id, None)
|
|
96
101
|
if task:
|
|
97
102
|
task.cancel()
|
|
98
|
-
print(f"[
|
|
103
|
+
print(f"[kernel] {module_id} disconnected")
|
|
99
104
|
|
|
100
105
|
# ── Sender loop (per-subscriber) ──
|
|
101
106
|
|
|
@@ -111,7 +116,7 @@ class EventHub:
|
|
|
111
116
|
await ws.send_text(raw)
|
|
112
117
|
self._cnt_routed += 1
|
|
113
118
|
except Exception:
|
|
114
|
-
print(f"[
|
|
119
|
+
print(f"[kernel] Send failed to {mid}, closing sender")
|
|
115
120
|
break
|
|
116
121
|
except asyncio.CancelledError:
|
|
117
122
|
pass
|
|
@@ -124,83 +129,72 @@ class EventHub:
|
|
|
124
129
|
self.subscriptions[module_id].update(
|
|
125
130
|
(p, tuple(p.split("."))) for p in events
|
|
126
131
|
)
|
|
127
|
-
print(f"[
|
|
132
|
+
print(f"[kernel] {module_id} subscribed: {events}")
|
|
128
133
|
|
|
129
134
|
def handle_unsubscribe(self, module_id: str, events: list[str]):
|
|
130
135
|
subs = self.subscriptions.get(module_id)
|
|
131
136
|
if subs:
|
|
132
137
|
to_remove = {item for item in subs if item[0] in events}
|
|
133
138
|
subs.difference_update(to_remove)
|
|
134
|
-
print(f"[
|
|
135
|
-
|
|
136
|
-
# ── Main message handler ──
|
|
137
|
-
|
|
138
|
-
async def handle_message(self, module_id: str, ws: WebSocket, raw: str):
|
|
139
|
-
try:
|
|
140
|
-
msg = _loads(raw)
|
|
141
|
-
except Exception:
|
|
142
|
-
await self._send_error(ws, "Invalid JSON")
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
msg_type = msg.get("type", "")
|
|
146
|
-
|
|
147
|
-
if msg_type == "subscribe":
|
|
148
|
-
events = msg.get("events", [])
|
|
149
|
-
if isinstance(events, list) and events:
|
|
150
|
-
self.handle_subscribe(module_id, events)
|
|
151
|
-
return
|
|
139
|
+
print(f"[kernel] {module_id} unsubscribed: {events}")
|
|
152
140
|
|
|
153
|
-
|
|
154
|
-
events = msg.get("events", [])
|
|
155
|
-
if isinstance(events, list) and events:
|
|
156
|
-
self.handle_unsubscribe(module_id, events)
|
|
157
|
-
return
|
|
141
|
+
# ── Event publishing (called by RpcRouter) ──
|
|
158
142
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
await self._send_error(ws, f"Unknown message type: {msg_type}")
|
|
164
|
-
|
|
165
|
-
# ── Event processing ──
|
|
166
|
-
|
|
167
|
-
async def _handle_event(self, module_id: str, ws: WebSocket, msg: dict):
|
|
168
|
-
"""Validate → dedup → auto-fill → route → ack."""
|
|
143
|
+
def publish_event(self, sender_id: str, event_id: str, event_type: str,
|
|
144
|
+
data: dict = None, echo: bool = False) -> dict:
|
|
145
|
+
"""Publish an event from a module. Called by RpcRouter for event.publish RPC.
|
|
146
|
+
Returns {ok: True} on success, or error dict if validation fails."""
|
|
169
147
|
self._cnt_received += 1
|
|
170
148
|
|
|
171
|
-
event_id = msg.get("event_id")
|
|
172
|
-
event_type = msg.get("event")
|
|
173
149
|
if not event_id or not event_type:
|
|
174
150
|
self._cnt_errors += 1
|
|
175
|
-
|
|
176
|
-
return
|
|
151
|
+
return {"ok": False, "error": "Missing required field: event_id or event"}
|
|
177
152
|
|
|
178
153
|
if self.dedup.is_duplicate(event_id):
|
|
179
154
|
self._cnt_dedup += 1
|
|
180
|
-
|
|
181
|
-
|
|
155
|
+
return {"ok": True} # silently accept duplicates
|
|
156
|
+
|
|
157
|
+
msg = {
|
|
158
|
+
"jsonrpc": "2.0",
|
|
159
|
+
"method": "event",
|
|
160
|
+
"params": {
|
|
161
|
+
"event_id": event_id,
|
|
162
|
+
"event": event_type,
|
|
163
|
+
"source": sender_id,
|
|
164
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
165
|
+
"data": data or {},
|
|
166
|
+
},
|
|
167
|
+
}
|
|
182
168
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if not msg.get("timestamp"):
|
|
186
|
-
msg["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
169
|
+
self._route_event(sender_id, msg, event_type, echo)
|
|
170
|
+
return {"ok": True}
|
|
187
171
|
|
|
188
|
-
|
|
189
|
-
|
|
172
|
+
def publish_internal(self, event_type: str, data: dict):
|
|
173
|
+
"""Publish a Kernel-originated event (e.g. module.offline, module.registered).
|
|
174
|
+
No dedup check — internal events are unique by nature."""
|
|
175
|
+
import uuid
|
|
176
|
+
event_id = str(uuid.uuid4())
|
|
177
|
+
self._cnt_received += 1
|
|
190
178
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
179
|
+
msg = {
|
|
180
|
+
"jsonrpc": "2.0",
|
|
181
|
+
"method": "event",
|
|
182
|
+
"params": {
|
|
183
|
+
"event_id": event_id,
|
|
184
|
+
"event": event_type,
|
|
185
|
+
"source": "kernel",
|
|
186
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
187
|
+
"data": data,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
self._route_event("kernel", msg, event_type, echo=False)
|
|
196
192
|
|
|
197
193
|
# ── Routing ──
|
|
198
194
|
|
|
199
|
-
|
|
195
|
+
def _route_event(self, sender_id: str, msg: dict, event_type: str, echo: bool):
|
|
200
196
|
"""Enqueue event to all matching subscribers' delivery queues."""
|
|
201
|
-
event_type = msg["event"]
|
|
202
197
|
e_parts = tuple(event_type.split("."))
|
|
203
|
-
echo = msg.get("echo", False)
|
|
204
198
|
raw = None # lazy serialization
|
|
205
199
|
|
|
206
200
|
for mid, patterns in self.subscriptions.items():
|
|
@@ -215,26 +209,12 @@ class EventHub:
|
|
|
215
209
|
try:
|
|
216
210
|
queue.put_nowait(raw)
|
|
217
211
|
except asyncio.QueueFull:
|
|
218
|
-
|
|
212
|
+
# Best-effort: drop if queue is full and we can't await
|
|
213
|
+
# (we're not in an async context in _route_event)
|
|
214
|
+
pass
|
|
219
215
|
self._cnt_queued += 1
|
|
220
216
|
break
|
|
221
217
|
|
|
222
|
-
# ── Helpers ──
|
|
223
|
-
|
|
224
|
-
@staticmethod
|
|
225
|
-
async def _send_ack(ws: WebSocket, event_id: str):
|
|
226
|
-
try:
|
|
227
|
-
await ws.send_text('{"type":"ack","event_id":"' + event_id + '"}')
|
|
228
|
-
except Exception:
|
|
229
|
-
pass
|
|
230
|
-
|
|
231
|
-
@staticmethod
|
|
232
|
-
async def _send_error(ws: WebSocket, message: str):
|
|
233
|
-
try:
|
|
234
|
-
await ws.send_json({"type": "error", "message": message})
|
|
235
|
-
except Exception:
|
|
236
|
-
pass
|
|
237
|
-
|
|
238
218
|
# ── Stats ──
|
|
239
219
|
|
|
240
220
|
def _counters_dict(self) -> dict:
|
package/kernel/module.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kernel
|
|
3
|
+
display_name: Kernel
|
|
4
|
+
type: infrastructure
|
|
5
|
+
state: enabled
|
|
6
|
+
runtime: python
|
|
7
|
+
entry: entry.py
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Kernel
|
|
11
|
+
|
|
12
|
+
Unified infrastructure module that merges Registry (service discovery) and Event Hub (event routing) into a single process with a WebSocket JSON-RPC 2.0 interface.
|
|
13
|
+
|
|
14
|
+
## Responsibilities
|
|
15
|
+
|
|
16
|
+
- **Service Registry**: Module registration, heartbeat TTL, glob lookup, dot-path queries
|
|
17
|
+
- **Event Routing**: NATS-style wildcard subscriptions, per-subscriber queues, 1h dedup
|
|
18
|
+
- **RPC Routing**: JSON-RPC 2.0 dispatch for builtin methods + cross-module forwarding
|
|
19
|
+
- **Token Verification**: In-memory token→module_id resolution (no cross-process HTTP)
|
|
20
|
+
|
|
21
|
+
## Protocol
|
|
22
|
+
|
|
23
|
+
Single WebSocket endpoint: `ws://127.0.0.1:{port}/ws?token={TOKEN}&id={MODULE_ID}`
|
|
24
|
+
|
|
25
|
+
Three frame types on the wire:
|
|
26
|
+
- **RPC Request**: `{jsonrpc:"2.0", id, method, params}` — client→Kernel or Kernel→client (forwarded)
|
|
27
|
+
- **RPC Response**: `{jsonrpc:"2.0", id, result}` or `{jsonrpc:"2.0", id, error}` — response to request
|
|
28
|
+
- **Event Notification**: `{jsonrpc:"2.0", method:"event", params:{event_id, event, source, timestamp, data}}` — no id, no response
|
|
29
|
+
|
|
30
|
+
## HTTP Endpoints (debug only)
|
|
31
|
+
|
|
32
|
+
- `GET /health` — combined registry + event hub health
|
|
33
|
+
- `GET /stats` — connections, subscriptions, counters
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Kernel registry in-memory store.
|
|
3
3
|
Manages module records, token verification, heartbeat TTL, lookup and get-by-path.
|
|
4
|
-
No persistence —
|
|
4
|
+
No persistence — Kernel crash triggers Kite full restart, all modules re-register.
|
|
5
|
+
|
|
6
|
+
Adapted from core/registry/store.py for the merged Kernel module:
|
|
7
|
+
- api_endpoint is now optional (modules no longer need HTTP)
|
|
8
|
+
- Added set_offline() for WebSocket disconnect handling
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
import fnmatch
|
|
@@ -50,7 +54,7 @@ class RegistryStore:
|
|
|
50
54
|
|
|
51
55
|
# ── Module lifecycle ──
|
|
52
56
|
|
|
53
|
-
_REQUIRED_FIELDS = ("module_id", "module_type"
|
|
57
|
+
_REQUIRED_FIELDS = ("module_id", "module_type") # api_endpoint now optional
|
|
54
58
|
|
|
55
59
|
def register_module(self, data: dict) -> dict:
|
|
56
60
|
"""Register or update a module. Idempotent — same module_id overwrites."""
|
|
@@ -69,7 +73,7 @@ class RegistryStore:
|
|
|
69
73
|
if other_mid == mid:
|
|
70
74
|
continue
|
|
71
75
|
if tool_name in other_data.get("tools", {}):
|
|
72
|
-
print(f"[
|
|
76
|
+
print(f"[kernel] WARNING: tool '{tool_name}' registered by both '{other_mid}' and '{mid}'")
|
|
73
77
|
|
|
74
78
|
# Strip action field — it's a request verb, not part of the registration payload
|
|
75
79
|
record = {k: v for k, v in data.items() if k != "action"}
|
|
@@ -93,6 +97,11 @@ class RegistryStore:
|
|
|
93
97
|
self.modules[module_id]["status"] = "online"
|
|
94
98
|
return {"ok": True}
|
|
95
99
|
|
|
100
|
+
def set_offline(self, module_id: str):
|
|
101
|
+
"""Mark a module as offline (called on WebSocket disconnect)."""
|
|
102
|
+
if module_id in self.modules:
|
|
103
|
+
self.modules[module_id]["status"] = "offline"
|
|
104
|
+
|
|
96
105
|
def check_ttl(self) -> list[str]:
|
|
97
106
|
"""Mark modules as offline if heartbeat expired. Returns list of newly-offline module_ids."""
|
|
98
107
|
now = time.time()
|