@agentunion/kite 1.0.7 → 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.
Files changed (100) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +48 -0
  3. package/cli.js +1 -1
  4. package/extensions/agents/__init__.py +1 -0
  5. package/extensions/agents/assistant/__init__.py +1 -0
  6. package/extensions/agents/assistant/entry.py +329 -0
  7. package/extensions/agents/assistant/module.md +22 -0
  8. package/extensions/agents/assistant/server.py +197 -0
  9. package/extensions/channels/__init__.py +1 -0
  10. package/extensions/channels/acp_channel/__init__.py +1 -0
  11. package/extensions/channels/acp_channel/entry.py +329 -0
  12. package/extensions/channels/acp_channel/module.md +22 -0
  13. package/extensions/channels/acp_channel/server.py +197 -0
  14. package/extensions/event_hub_bench/entry.py +624 -379
  15. package/extensions/event_hub_bench/module.md +2 -1
  16. package/extensions/services/backup/__init__.py +1 -0
  17. package/extensions/services/backup/entry.py +508 -0
  18. package/extensions/services/backup/module.md +22 -0
  19. package/extensions/services/model_service/__init__.py +1 -0
  20. package/extensions/services/model_service/entry.py +508 -0
  21. package/extensions/services/model_service/module.md +22 -0
  22. package/extensions/services/watchdog/entry.py +468 -102
  23. package/extensions/services/watchdog/module.md +3 -0
  24. package/extensions/services/watchdog/monitor.py +170 -69
  25. package/extensions/services/web/__init__.py +1 -0
  26. package/extensions/services/web/config.yaml +149 -0
  27. package/extensions/services/web/entry.py +390 -0
  28. package/extensions/services/web/module.md +24 -0
  29. package/extensions/services/web/routes/__init__.py +1 -0
  30. package/extensions/services/web/routes/routes_call.py +189 -0
  31. package/extensions/services/web/routes/routes_config.py +512 -0
  32. package/extensions/services/web/routes/routes_contacts.py +98 -0
  33. package/extensions/services/web/routes/routes_devlog.py +99 -0
  34. package/extensions/services/web/routes/routes_phone.py +81 -0
  35. package/extensions/services/web/routes/routes_sms.py +48 -0
  36. package/extensions/services/web/routes/routes_stats.py +17 -0
  37. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  38. package/extensions/services/web/routes/schemas.py +216 -0
  39. package/extensions/services/web/server.py +375 -0
  40. package/extensions/services/web/static/css/style.css +1064 -0
  41. package/extensions/services/web/static/index.html +1445 -0
  42. package/extensions/services/web/static/js/app.js +4671 -0
  43. package/extensions/services/web/vendor/__init__.py +1 -0
  44. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  45. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  46. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  47. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  48. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  49. package/extensions/services/web/vendor/config.py +139 -0
  50. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  51. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  52. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  53. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  54. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  55. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  56. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  57. package/extensions/services/web/vendor/storage/identity.py +312 -0
  58. package/extensions/services/web/vendor/storage/store.py +507 -0
  59. package/extensions/services/web/vendor/task/manager.py +864 -0
  60. package/extensions/services/web/vendor/task/models.py +45 -0
  61. package/extensions/services/web/vendor/task/webhook.py +263 -0
  62. package/extensions/services/web/vendor/tools/registry.py +321 -0
  63. package/kernel/__init__.py +0 -0
  64. package/kernel/entry.py +407 -0
  65. package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
  66. package/kernel/module.md +33 -0
  67. package/{core/registry/store.py → kernel/registry_store.py} +23 -8
  68. package/kernel/rpc_router.py +388 -0
  69. package/kernel/server.py +267 -0
  70. package/launcher/__init__.py +10 -0
  71. package/launcher/__main__.py +6 -0
  72. package/launcher/count_lines.py +258 -0
  73. package/launcher/entry.py +1778 -0
  74. package/launcher/logging_setup.py +289 -0
  75. package/{core/launcher → launcher}/module_scanner.py +11 -6
  76. package/launcher/process_manager.py +880 -0
  77. package/main.py +11 -210
  78. package/package.json +6 -9
  79. package/__init__.py +0 -1
  80. package/__main__.py +0 -15
  81. package/core/event_hub/BENCHMARK.md +0 -94
  82. package/core/event_hub/bench.py +0 -459
  83. package/core/event_hub/bench_extreme.py +0 -308
  84. package/core/event_hub/bench_perf.py +0 -350
  85. package/core/event_hub/entry.py +0 -157
  86. package/core/event_hub/module.md +0 -20
  87. package/core/event_hub/server.py +0 -206
  88. package/core/launcher/entry.py +0 -1158
  89. package/core/launcher/process_manager.py +0 -470
  90. package/core/registry/entry.py +0 -110
  91. package/core/registry/module.md +0 -30
  92. package/core/registry/server.py +0 -289
  93. package/extensions/services/watchdog/server.py +0 -167
  94. /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
  95. /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
  96. /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
  97. /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
  98. /package/{core/event_hub → kernel}/dedup.py +0 -0
  99. /package/{core/event_hub → kernel}/router.py +0 -0
  100. /package/{core/launcher → launcher}/module.md +0 -0
@@ -1,7 +1,11 @@
1
1
  """
2
- Registry in-memory store.
2
+ Kernel registry in-memory store.
3
3
  Manages module records, token verification, heartbeat TTL, lookup and get-by-path.
4
- No persistence — Registry crash triggers Kite full restart, all modules re-register.
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
@@ -25,18 +29,24 @@ class RegistryStore:
25
29
 
26
30
  def verify_token(self, token: str) -> str | None:
27
31
  """Return module_id if token is valid, None otherwise.
28
- In debug mode (KITE_DEBUG=1), any non-empty token is accepted."""
29
- if self.is_debug and token:
30
- return "debug"
32
+ In debug mode (KITE_DEBUG=1), any non-empty token is accepted,
33
+ but known tokens still resolve to their proper module_id first."""
31
34
  if token == self.launcher_token:
32
35
  return "launcher"
33
36
  for mid, tok in self.token_map.items():
34
37
  if token == tok:
35
38
  return mid
39
+ if self.is_debug and token:
40
+ return "debug"
36
41
  return None
37
42
 
38
43
  def is_launcher(self, token: str) -> bool:
39
- return token == self.launcher_token
44
+ """Check if token belongs to Launcher. In debug mode, also accept any token."""
45
+ if token == self.launcher_token:
46
+ return True
47
+ if self.is_debug and token:
48
+ return True
49
+ return False
40
50
 
41
51
  def register_tokens(self, mapping: dict[str, str]):
42
52
  """Register module_id -> token mapping. Only Launcher may call this."""
@@ -44,7 +54,7 @@ class RegistryStore:
44
54
 
45
55
  # ── Module lifecycle ──
46
56
 
47
- _REQUIRED_FIELDS = ("module_id", "module_type", "api_endpoint")
57
+ _REQUIRED_FIELDS = ("module_id", "module_type") # api_endpoint now optional
48
58
 
49
59
  def register_module(self, data: dict) -> dict:
50
60
  """Register or update a module. Idempotent — same module_id overwrites."""
@@ -63,7 +73,7 @@ class RegistryStore:
63
73
  if other_mid == mid:
64
74
  continue
65
75
  if tool_name in other_data.get("tools", {}):
66
- print(f"[registry] WARNING: tool '{tool_name}' registered by both '{other_mid}' and '{mid}'")
76
+ print(f"[kernel] WARNING: tool '{tool_name}' registered by both '{other_mid}' and '{mid}'")
67
77
 
68
78
  # Strip action field — it's a request verb, not part of the registration payload
69
79
  record = {k: v for k, v in data.items() if k != "action"}
@@ -87,6 +97,11 @@ class RegistryStore:
87
97
  self.modules[module_id]["status"] = "online"
88
98
  return {"ok": True}
89
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
+
90
105
  def check_ttl(self) -> list[str]:
91
106
  """Mark modules as offline if heartbeat expired. Returns list of newly-offline module_ids."""
92
107
  now = time.time()
@@ -0,0 +1,388 @@
1
+ """
2
+ JSON-RPC 2.0 message dispatcher for Kernel.
3
+ Routes builtin methods (registry.*, event.*, kernel.*) and forwards
4
+ cross-module RPC requests with timeout tracking.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import time
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
13
+
14
+ from starlette.websockets import WebSocket
15
+
16
+ from .registry_store import RegistryStore
17
+ from .event_hub import EventHub
18
+
19
+
20
+ # ── JSON-RPC 2.0 error codes ──
21
+
22
+ PARSE_ERROR = -32700
23
+ INVALID_REQUEST = -32600
24
+ METHOD_NOT_FOUND = -32601
25
+ INVALID_PARAMS = -32602
26
+ INTERNAL_ERROR = -32603
27
+
28
+ # Kite custom error codes
29
+ MODULE_OFFLINE = -32001
30
+ RPC_TIMEOUT = -32002
31
+ AUTH_FAILED = -32003
32
+ PERMISSION_DENIED = -32004
33
+ DUPLICATE_EVENT = -32005
34
+
35
+ DEFAULT_FORWARD_TIMEOUT = 5.0 # seconds
36
+
37
+
38
+ @dataclass
39
+ class PendingForward:
40
+ """Tracks a forwarded cross-module RPC awaiting response."""
41
+ caller_ws: WebSocket
42
+ original_id: str
43
+ caller_id: str
44
+ target_id: str
45
+ created_at: float = field(default_factory=time.time)
46
+ timeout_handle: asyncio.TimerHandle | None = None
47
+
48
+
49
+ def _result_msg(msg_id: str, result: dict) -> str:
50
+ return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result})
51
+
52
+
53
+ def _error_msg(msg_id: str, code: int, message: str, data: dict = None) -> str:
54
+ err = {"code": code, "message": message}
55
+ if data:
56
+ err["data"] = data
57
+ return json.dumps({"jsonrpc": "2.0", "id": msg_id, "error": err})
58
+
59
+
60
+ class RpcRouter:
61
+ """JSON-RPC 2.0 message dispatcher.
62
+
63
+ Handles:
64
+ - 13 builtin methods (registry.*, event.*, kernel.*)
65
+ - Cross-module RPC forwarding with timeout
66
+ - RPC response matching for forwarded calls
67
+ """
68
+
69
+ def __init__(self, registry: RegistryStore, event_hub: EventHub,
70
+ connections: dict[str, WebSocket], kernel_server):
71
+ self.registry = registry
72
+ self.event_hub = event_hub
73
+ self.connections = connections
74
+ self.kernel_server = kernel_server # Direct reference to KernelServer
75
+
76
+ # Builtin method dispatch table
77
+ self.methods: dict[str, callable] = {
78
+ "registry.register": self._registry_register,
79
+ "registry.deregister": self._registry_deregister,
80
+ "registry.heartbeat": self._registry_heartbeat,
81
+ "registry.lookup": self._registry_lookup,
82
+ "registry.get": self._registry_get,
83
+ "registry.verify": self._registry_verify,
84
+ "event.publish": self._event_publish,
85
+ "event.subscribe": self._event_subscribe,
86
+ "event.unsubscribe": self._event_unsubscribe,
87
+ "kernel.ping": self._kernel_ping,
88
+ "kernel.stats": self._kernel_stats,
89
+ "kernel.health": self._kernel_health,
90
+ "kernel.generate_tokens": self._kernel_generate_tokens,
91
+ "kernel.register_tokens": self._kernel_register_tokens,
92
+ "kernel.shutdown": self._kernel_shutdown,
93
+ }
94
+
95
+ # Pending cross-module forwards: internal_id -> PendingForward
96
+ self._pending: dict[str, PendingForward] = {}
97
+
98
+ # ── Main dispatch ──
99
+
100
+ async def dispatch(self, caller_id: str, ws: WebSocket, msg: dict):
101
+ """Route a parsed JSON-RPC message to builtin handler or cross-module forward."""
102
+ method = msg.get("method", "")
103
+ msg_id = msg.get("id")
104
+
105
+ # JSON-RPC Notification (no id) — currently not handled from clients
106
+ if msg_id is None:
107
+ return
108
+
109
+ params = msg.get("params") or {}
110
+
111
+ # Builtin method
112
+ handler = self.methods.get(method)
113
+ if handler:
114
+ try:
115
+ result = await handler(caller_id, params)
116
+ await ws.send_text(_result_msg(msg_id, result))
117
+ except Exception as e:
118
+ print(f"[kernel] RPC handler error ({method}): {e}")
119
+ await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
120
+ return
121
+
122
+ # Cross-module forward: method prefix is target module_id
123
+ dot_idx = method.find(".")
124
+ if dot_idx > 0:
125
+ target = method[:dot_idx]
126
+ if target in self.connections:
127
+ await self._forward(caller_id, ws, msg_id, target, method, params)
128
+ return
129
+ # Target not connected — check if registered but offline
130
+ if target in self.registry.modules:
131
+ await ws.send_text(_error_msg(
132
+ msg_id, MODULE_OFFLINE, f"Module offline: {target}"))
133
+ return
134
+
135
+ # Method not found
136
+ await ws.send_text(_error_msg(msg_id, METHOD_NOT_FOUND, f"Method not found: {method}"))
137
+
138
+ async def handle_response(self, module_id: str, msg: dict):
139
+ """Handle an RPC response from a module (matches pending forwards)."""
140
+ msg_id = msg.get("id")
141
+ if not msg_id:
142
+ return
143
+
144
+ pending = self._pending.pop(msg_id, None)
145
+ if not pending:
146
+ return # orphan response, ignore
147
+
148
+ # Cancel timeout
149
+ if pending.timeout_handle:
150
+ pending.timeout_handle.cancel()
151
+
152
+ # Forward response to original caller with original ID
153
+ response = {"jsonrpc": "2.0", "id": pending.original_id}
154
+ if "result" in msg:
155
+ response["result"] = msg["result"]
156
+ elif "error" in msg:
157
+ response["error"] = msg["error"]
158
+ else:
159
+ response["result"] = None
160
+
161
+ try:
162
+ await pending.caller_ws.send_text(json.dumps(response))
163
+ except Exception:
164
+ pass # caller disconnected
165
+
166
+ # ── Cross-module forwarding ──
167
+
168
+ async def _forward(self, caller_id: str, caller_ws: WebSocket,
169
+ original_id: str, target: str, method: str, params: dict):
170
+ """Forward RPC request to target module with timeout tracking."""
171
+ internal_id = f"fwd-{uuid.uuid4().hex[:12]}"
172
+
173
+ # Extract timeout from params (optional _timeout field)
174
+ timeout = DEFAULT_FORWARD_TIMEOUT
175
+ if isinstance(params, dict) and "_timeout" in params:
176
+ try:
177
+ timeout = float(params.pop("_timeout"))
178
+ except (ValueError, TypeError):
179
+ pass
180
+
181
+ # Strip target prefix from method for the forwarded request
182
+ actual_method = method[len(target) + 1:] # e.g. "watchdog.get_status" -> "get_status"
183
+
184
+ # Record pending forward
185
+ loop = asyncio.get_event_loop()
186
+ pending = PendingForward(
187
+ caller_ws=caller_ws,
188
+ original_id=original_id,
189
+ caller_id=caller_id,
190
+ target_id=target,
191
+ )
192
+ pending.timeout_handle = loop.call_later(
193
+ timeout, lambda iid=internal_id: asyncio.ensure_future(self._handle_timeout(iid))
194
+ )
195
+ self._pending[internal_id] = pending
196
+
197
+ # Send to target
198
+ fwd_msg = json.dumps({
199
+ "jsonrpc": "2.0",
200
+ "id": internal_id,
201
+ "method": actual_method,
202
+ "params": params or {},
203
+ })
204
+ target_ws = self.connections.get(target)
205
+ if target_ws:
206
+ try:
207
+ await target_ws.send_text(fwd_msg)
208
+ except Exception:
209
+ # Target send failed — clean up and error to caller
210
+ self._pending.pop(internal_id, None)
211
+ if pending.timeout_handle:
212
+ pending.timeout_handle.cancel()
213
+ await caller_ws.send_text(_error_msg(
214
+ original_id, MODULE_OFFLINE, f"Failed to reach module: {target}"))
215
+ else:
216
+ # Target disconnected between check and send
217
+ self._pending.pop(internal_id, None)
218
+ if pending.timeout_handle:
219
+ pending.timeout_handle.cancel()
220
+ await caller_ws.send_text(_error_msg(
221
+ original_id, MODULE_OFFLINE, f"Module offline: {target}"))
222
+
223
+ async def _handle_timeout(self, internal_id: str):
224
+ """Called when a forwarded RPC times out."""
225
+ pending = self._pending.pop(internal_id, None)
226
+ if not pending:
227
+ return
228
+ try:
229
+ await pending.caller_ws.send_text(_error_msg(
230
+ pending.original_id, RPC_TIMEOUT,
231
+ f"RPC timeout waiting for {pending.target_id}"))
232
+ except Exception:
233
+ pass
234
+
235
+ # ── Builtin handlers: registry.* ──
236
+
237
+ async def _registry_register(self, caller_id: str, params: dict) -> dict:
238
+ mid = params.get("module_id")
239
+ if not mid:
240
+ return {"ok": False, "error": "module_id required"}
241
+ # Permission: only Launcher or the module itself
242
+ if caller_id != "launcher" and caller_id != mid:
243
+ return {"ok": False, "error": f"Module '{caller_id}' cannot register as '{mid}'"}
244
+ result = self.registry.register_module(params)
245
+ if result.get("ok"):
246
+ self.event_hub.publish_internal("module.registered", {"module_id": mid})
247
+
248
+ # When Launcher registers, Kernel publishes its own module.ready
249
+ if mid == "launcher" and self.kernel_server:
250
+ if not self.kernel_server._ready_published:
251
+ self.kernel_server.publish_ready()
252
+ self.kernel_server._ready_published = True
253
+ print(f"[kernel] launcher registered → kernel module.ready published")
254
+ return result
255
+
256
+ async def _registry_deregister(self, caller_id: str, params: dict) -> dict:
257
+ mid = params.get("module_id")
258
+ if not mid:
259
+ return {"ok": False, "error": "module_id required"}
260
+ if caller_id != "launcher" and caller_id != mid:
261
+ return {"ok": False, "error": f"Module '{caller_id}' cannot deregister '{mid}'"}
262
+ result = self.registry.deregister_module(mid)
263
+ if result.get("ok"):
264
+ self.event_hub.publish_internal("module.unregistered", {"module_id": mid})
265
+ return result
266
+
267
+ async def _registry_heartbeat(self, caller_id: str, params: dict) -> dict:
268
+ mid = params.get("module_id")
269
+ if not mid:
270
+ return {"ok": False, "error": "module_id required"}
271
+ if caller_id != "launcher" and caller_id != mid:
272
+ return {"ok": False, "error": f"Module '{caller_id}' cannot heartbeat for '{mid}'"}
273
+ return self.registry.heartbeat(mid)
274
+
275
+ async def _registry_lookup(self, caller_id: str, params: dict) -> dict:
276
+ results = self.registry.lookup(
277
+ field=params.get("field"),
278
+ module=params.get("module"),
279
+ value=params.get("value"),
280
+ )
281
+ return {"ok": True, "results": results}
282
+
283
+ async def _registry_get(self, caller_id: str, params: dict) -> dict:
284
+ path = params.get("path", "")
285
+ if not path:
286
+ return {"ok": False, "error": "path required"}
287
+ val, found = self.registry.get_by_path(path)
288
+ if not found:
289
+ return {"ok": False, "error": f"Path not found: {path}"}
290
+ return {"ok": True, "value": val}
291
+
292
+ async def _registry_verify(self, caller_id: str, params: dict) -> dict:
293
+ token = params.get("token", "")
294
+ module_id = self.registry.verify_token(token)
295
+ if module_id:
296
+ return {"ok": True, "module_id": module_id}
297
+ return {"ok": False}
298
+
299
+ # ── Builtin handlers: event.* ──
300
+
301
+ async def _event_publish(self, caller_id: str, params: dict) -> dict:
302
+ event_id = params.get("event_id", "")
303
+ event_type = params.get("event", "")
304
+ data = params.get("data")
305
+ echo = params.get("echo", False)
306
+ return self.event_hub.publish_event(caller_id, event_id, event_type, data, echo)
307
+
308
+ async def _event_subscribe(self, caller_id: str, params: dict) -> dict:
309
+ events = params.get("events", [])
310
+ if not isinstance(events, list) or not events:
311
+ return {"ok": False, "error": "events must be a non-empty list"}
312
+ self.event_hub.handle_subscribe(caller_id, events)
313
+ return {"ok": True}
314
+
315
+ async def _event_unsubscribe(self, caller_id: str, params: dict) -> dict:
316
+ events = params.get("events", [])
317
+ if not isinstance(events, list) or not events:
318
+ return {"ok": False, "error": "events must be a non-empty list"}
319
+ self.event_hub.handle_unsubscribe(caller_id, events)
320
+ return {"ok": True}
321
+
322
+ # ── Builtin handlers: kernel.* ──
323
+
324
+ async def _kernel_ping(self, caller_id: str, params: dict) -> dict:
325
+ return {"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()}
326
+
327
+ async def _kernel_stats(self, caller_id: str, params: dict) -> dict:
328
+ return self.event_hub.get_stats()
329
+
330
+ async def _kernel_health(self, caller_id: str, params: dict) -> dict:
331
+ eh_health = self.event_hub.get_health()
332
+ return {
333
+ "status": "healthy",
334
+ "module_count": len(self.registry.modules),
335
+ "online_count": sum(
336
+ 1 for m in self.registry.modules.values()
337
+ if m.get("status") == "online"
338
+ ),
339
+ "event_stats": eh_health.get("details", {}),
340
+ }
341
+
342
+ async def _kernel_generate_tokens(self, caller_id: str, params: dict) -> dict:
343
+ """Generate tokens for a list of module names.
344
+
345
+ Args:
346
+ params: {"modules": ["mod1", "mod2", ...]}
347
+
348
+ Returns:
349
+ {"ok": True, "tokens": {"mod1": "token1", "mod2": "token2", ...}}
350
+ """
351
+ # Only Launcher may request token generation
352
+ if caller_id != "launcher":
353
+ return {"ok": False, "error": "Only Launcher may generate tokens"}
354
+
355
+ modules = params.get("modules", [])
356
+ if not isinstance(modules, list):
357
+ return {"ok": False, "error": "modules must be a list"}
358
+
359
+ import secrets
360
+ tokens = {}
361
+ for module_name in modules:
362
+ tokens[module_name] = secrets.token_hex(32)
363
+
364
+ # Register tokens in registry
365
+ self.registry.register_tokens(tokens)
366
+
367
+ return {"ok": True, "tokens": tokens}
368
+
369
+ async def _kernel_register_tokens(self, caller_id: str, params: dict) -> dict:
370
+ # Only Launcher may register tokens
371
+ if caller_id != "launcher":
372
+ return {"ok": False, "error": "Only Launcher may register tokens"}
373
+ self.registry.register_tokens(params)
374
+ return {"ok": True}
375
+
376
+ async def _kernel_shutdown(self, caller_id: str, params: dict) -> dict:
377
+ """Shutdown Kernel. Only Launcher may call this."""
378
+ if caller_id != "launcher":
379
+ return {"ok": False, "error": "Only Launcher may shutdown Kernel"}
380
+
381
+ print("[kernel] Received shutdown request from Launcher")
382
+
383
+ # Schedule shutdown (don't block RPC response)
384
+ if self.kernel_server:
385
+ asyncio.create_task(self.kernel_server.shutdown())
386
+
387
+ return {"ok": True}
388
+