@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.
- package/CHANGELOG.md +208 -0
- package/README.md +48 -0
- package/cli.js +1 -1
- package/extensions/agents/__init__.py +1 -0
- package/extensions/agents/assistant/__init__.py +1 -0
- package/extensions/agents/assistant/entry.py +329 -0
- package/extensions/agents/assistant/module.md +22 -0
- package/extensions/agents/assistant/server.py +197 -0
- package/extensions/channels/__init__.py +1 -0
- package/extensions/channels/acp_channel/__init__.py +1 -0
- package/extensions/channels/acp_channel/entry.py +329 -0
- package/extensions/channels/acp_channel/module.md +22 -0
- package/extensions/channels/acp_channel/server.py +197 -0
- package/extensions/event_hub_bench/entry.py +624 -379
- package/extensions/event_hub_bench/module.md +2 -1
- package/extensions/services/backup/__init__.py +1 -0
- package/extensions/services/backup/entry.py +508 -0
- package/extensions/services/backup/module.md +22 -0
- package/extensions/services/model_service/__init__.py +1 -0
- package/extensions/services/model_service/entry.py +508 -0
- package/extensions/services/model_service/module.md +22 -0
- package/extensions/services/watchdog/entry.py +468 -102
- package/extensions/services/watchdog/module.md +3 -0
- package/extensions/services/watchdog/monitor.py +170 -69
- package/extensions/services/web/__init__.py +1 -0
- package/extensions/services/web/config.yaml +149 -0
- package/extensions/services/web/entry.py +390 -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 +375 -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/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/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/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/registry.py +321 -0
- package/kernel/__init__.py +0 -0
- package/kernel/entry.py +407 -0
- package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +23 -8
- 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/launcher/entry.py +1778 -0
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/launcher/process_manager.py +880 -0
- package/main.py +11 -210
- 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/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 -157
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -206
- package/core/launcher/entry.py +0 -1158
- package/core/launcher/process_manager.py +0 -470
- package/core/registry/entry.py +0 -110
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -289
- package/extensions/services/watchdog/server.py +0 -167
- /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
- /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
- /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
- /package/{core/registry → extensions/services/web/vendor/tools}/__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
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|
|
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"[
|
|
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
|
+
|