@agentunion/kite 1.3.2 → 1.4.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 +200 -0
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -215
- package/extensions/channels/acp_channel/entry.py +111 -1
- package/extensions/channels/acp_channel/module.md +23 -22
- package/extensions/channels/acp_channel/server.py +263 -215
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +299 -21
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +145 -19
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +188 -25
- package/extensions/services/watchdog/monitor.py +144 -34
- package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
- package/extensions/services/web/config_example.py +35 -0
- package/extensions/services/web/config_loader.py +110 -0
- package/extensions/services/web/entry.py +114 -26
- package/extensions/services/web/module.md +35 -24
- package/extensions/services/web/pairing.py +250 -0
- package/extensions/services/web/pairing_codes.jsonl +16 -0
- package/extensions/services/web/relay.py +643 -0
- package/extensions/services/web/relay_config.json5 +67 -0
- package/extensions/services/web/routes/routes_management_ws.py +127 -0
- package/extensions/services/web/routes/routes_rpc.py +89 -0
- package/extensions/services/web/routes/routes_test.py +61 -0
- package/extensions/services/web/routes/schemas.py +0 -22
- package/extensions/services/web/server.py +421 -98
- package/extensions/services/web/static/css/style.css +67 -28
- package/extensions/services/web/static/index.html +234 -44
- package/extensions/services/web/static/js/app.js +1335 -48
- package/extensions/services/web/static/js/kernel-client-example.js +161 -0
- package/extensions/services/web/static/js/kernel-client.js +383 -0
- package/extensions/services/web/static/js/registry-tests.js +558 -0
- package/extensions/services/web/static/js/token-manager.js +175 -0
- package/extensions/services/web/static/pairing.html +248 -0
- package/extensions/services/web/static/test_registry.html +262 -0
- package/extensions/services/web/web_config.json5 +29 -0
- package/kernel/entry.py +120 -32
- package/kernel/event_hub.py +141 -16
- package/kernel/module.md +36 -33
- package/kernel/registry_store.py +48 -15
- package/kernel/rpc_router.py +120 -53
- package/kernel/server.py +219 -12
- package/kite_cli/__init__.py +3 -0
- package/kite_cli/__main__.py +5 -0
- package/kite_cli/commands/__init__.py +1 -0
- package/kite_cli/commands/clean.py +101 -0
- package/kite_cli/commands/doctor.py +35 -0
- package/kite_cli/commands/history.py +111 -0
- package/kite_cli/commands/info.py +96 -0
- package/kite_cli/commands/install.py +313 -0
- package/kite_cli/commands/list.py +143 -0
- package/kite_cli/commands/log.py +81 -0
- package/kite_cli/commands/rollback.py +88 -0
- package/kite_cli/commands/search.py +73 -0
- package/kite_cli/commands/uninstall.py +85 -0
- package/kite_cli/commands/update.py +118 -0
- package/kite_cli/core/__init__.py +1 -0
- package/kite_cli/core/checker.py +142 -0
- package/kite_cli/core/dependency.py +229 -0
- package/kite_cli/core/downloader.py +209 -0
- package/kite_cli/core/install_info.py +40 -0
- package/kite_cli/core/tool_installer.py +397 -0
- package/kite_cli/core/validator.py +78 -0
- package/kite_cli/main.py +289 -0
- package/kite_cli/utils/__init__.py +1 -0
- package/kite_cli/utils/i18n.py +252 -0
- package/kite_cli/utils/interactive.py +63 -0
- package/kite_cli/utils/operation_log.py +77 -0
- package/kite_cli/utils/paths.py +34 -0
- package/kite_cli/utils/version.py +308 -0
- package/launcher/entry.py +819 -158
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/package.json +2 -1
- package/scripts/plan_manager.py +315 -0
- package/extensions/services/web/routes/routes_modules.py +0 -249
package/kernel/registry_store.py
CHANGED
|
@@ -24,6 +24,7 @@ class RegistryStore:
|
|
|
24
24
|
self.ttl = 60 # seconds before marking offline
|
|
25
25
|
self.heartbeat_interval = 30
|
|
26
26
|
self.is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
27
|
+
self.last_update_time = time.time() # Global registry update timestamp
|
|
27
28
|
|
|
28
29
|
# ── Token verification ──
|
|
29
30
|
|
|
@@ -57,45 +58,82 @@ class RegistryStore:
|
|
|
57
58
|
_REQUIRED_FIELDS = ("module_id", "module_type") # api_endpoint now optional
|
|
58
59
|
|
|
59
60
|
def register_module(self, data: dict) -> dict:
|
|
60
|
-
"""Register or update a module. Idempotent — same module_id overwrites.
|
|
61
|
+
"""Register or update a module. Idempotent — same module_id overwrites.
|
|
62
|
+
Returns dict with ttl, heartbeat_interval, and changed flag on success, raises exception on failure."""
|
|
61
63
|
# Validate required fields
|
|
62
64
|
missing = [f for f in self._REQUIRED_FIELDS if not data.get(f)]
|
|
63
65
|
if missing:
|
|
64
|
-
|
|
66
|
+
raise ValueError(f"Missing required fields: {', '.join(missing)}")
|
|
65
67
|
|
|
66
68
|
mid = data["module_id"]
|
|
67
69
|
|
|
68
|
-
#
|
|
70
|
+
# Validate nested dict structure for lookup-able fields
|
|
71
|
+
# Only check known wrapper fields (tools, hooks, events_publish)
|
|
72
|
+
# RPC methods are registered directly at top level with their actual names
|
|
73
|
+
# events_subscribe is a list, not a dict
|
|
74
|
+
for field_name in ["tools", "hooks", "events_publish"]:
|
|
75
|
+
field_value = data.get(field_name)
|
|
76
|
+
if field_value is not None:
|
|
77
|
+
if not isinstance(field_value, dict):
|
|
78
|
+
raise TypeError(f"Field '{field_name}' must be a dict (nested structure), got {type(field_value).__name__}")
|
|
79
|
+
# Check if it's a flat dict with dot-notation keys (common mistake)
|
|
80
|
+
if any("." in str(k) for k in field_value.keys()):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Field '{field_name}' contains dot-notation keys (e.g., 'module.config.get'). "
|
|
83
|
+
f"Use nested dict structure instead: {{'module': {{'config': {{'get': ...}}}}}}. "
|
|
84
|
+
f"See docs/模块开发指南.md section 4.3 for correct format."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Warn on tool name conflicts (skip 'rpc' — it's a standard tool every module registers)
|
|
69
88
|
new_tools = data.get("tools", {})
|
|
70
89
|
if isinstance(new_tools, dict):
|
|
71
90
|
for tool_name in new_tools:
|
|
91
|
+
if tool_name == "rpc":
|
|
92
|
+
continue
|
|
72
93
|
for other_mid, other_data in self.modules.items():
|
|
73
94
|
if other_mid == mid:
|
|
74
95
|
continue
|
|
75
96
|
if tool_name in other_data.get("tools", {}):
|
|
76
97
|
print(f"[kernel] WARNING: tool '{tool_name}' registered by both '{other_mid}' and '{mid}'")
|
|
77
98
|
|
|
99
|
+
# Check if content changed (compare with existing registration)
|
|
100
|
+
changed = True
|
|
101
|
+
old_record = self.modules.get(mid)
|
|
102
|
+
if old_record:
|
|
103
|
+
# Strip action field from new data
|
|
104
|
+
new_record = {k: v for k, v in data.items() if k != "action"}
|
|
105
|
+
# Compare without status and registered_at fields
|
|
106
|
+
old_data = {k: v for k, v in old_record.items() if k not in ("status", "registered_at")}
|
|
107
|
+
changed = (new_record != old_data)
|
|
108
|
+
|
|
78
109
|
# Strip action field — it's a request verb, not part of the registration payload
|
|
79
110
|
record = {k: v for k, v in data.items() if k != "action"}
|
|
80
111
|
record["status"] = "registered" # State machine: connected → registered (via register RPC)
|
|
81
112
|
record["registered_at"] = time.time()
|
|
82
113
|
self.modules[mid] = record
|
|
83
114
|
self.heartbeats[mid] = time.time()
|
|
84
|
-
|
|
115
|
+
|
|
116
|
+
# Update global timestamp if content changed
|
|
117
|
+
if changed:
|
|
118
|
+
self.last_update_time = time.time()
|
|
119
|
+
|
|
120
|
+
return {"ttl": self.ttl, "heartbeat_interval": self.heartbeat_interval, "changed": changed}
|
|
85
121
|
|
|
86
122
|
def deregister_module(self, module_id: str) -> dict:
|
|
87
|
-
"""Remove a module record immediately."""
|
|
123
|
+
"""Remove a module record immediately. Returns empty dict."""
|
|
88
124
|
self.modules.pop(module_id, None)
|
|
89
125
|
self.heartbeats.pop(module_id, None)
|
|
90
|
-
|
|
126
|
+
# Update global timestamp
|
|
127
|
+
self.last_update_time = time.time()
|
|
128
|
+
return {}
|
|
91
129
|
|
|
92
130
|
def heartbeat(self, module_id: str) -> dict:
|
|
93
|
-
"""Renew heartbeat for a module."""
|
|
131
|
+
"""Renew heartbeat for a module. Returns empty dict on success, raises exception on failure."""
|
|
94
132
|
if module_id not in self.modules:
|
|
95
|
-
|
|
133
|
+
raise KeyError(f"Module '{module_id}' not registered")
|
|
96
134
|
self.heartbeats[module_id] = time.time()
|
|
97
135
|
# Don't change status — heartbeat just keeps alive, doesn't upgrade state
|
|
98
|
-
return {
|
|
136
|
+
return {}
|
|
99
137
|
|
|
100
138
|
def set_connected(self, module_id: str):
|
|
101
139
|
"""Mark a module as connected (WS established, not yet registered).
|
|
@@ -177,7 +215,7 @@ class RegistryStore:
|
|
|
177
215
|
def lookup(self, field: str = None, module: str = None, value: str = None) -> list[dict]:
|
|
178
216
|
"""
|
|
179
217
|
Search across all online modules. All three params support glob patterns.
|
|
180
|
-
Returns list of {field, module,
|
|
218
|
+
Returns list of {field, module, value}.
|
|
181
219
|
"""
|
|
182
220
|
results = []
|
|
183
221
|
for mid, data in self.modules.items():
|
|
@@ -186,8 +224,6 @@ class RegistryStore:
|
|
|
186
224
|
if module and not fnmatch.fnmatch(mid, module):
|
|
187
225
|
continue
|
|
188
226
|
|
|
189
|
-
api_ep = data.get("api_endpoint", "")
|
|
190
|
-
|
|
191
227
|
if field:
|
|
192
228
|
matches = self._match_fields(data, field)
|
|
193
229
|
for fpath, fval in matches:
|
|
@@ -196,7 +232,6 @@ class RegistryStore:
|
|
|
196
232
|
results.append({
|
|
197
233
|
"field": fpath,
|
|
198
234
|
"module": mid,
|
|
199
|
-
"api_endpoint": api_ep,
|
|
200
235
|
"value": fval,
|
|
201
236
|
})
|
|
202
237
|
elif value:
|
|
@@ -205,14 +240,12 @@ class RegistryStore:
|
|
|
205
240
|
results.append({
|
|
206
241
|
"field": k,
|
|
207
242
|
"module": mid,
|
|
208
|
-
"api_endpoint": api_ep,
|
|
209
243
|
"value": v,
|
|
210
244
|
})
|
|
211
245
|
else:
|
|
212
246
|
results.append({
|
|
213
247
|
"field": "module_id",
|
|
214
248
|
"module": mid,
|
|
215
|
-
"api_endpoint": api_ep,
|
|
216
249
|
"value": mid,
|
|
217
250
|
})
|
|
218
251
|
|
package/kernel/rpc_router.py
CHANGED
|
@@ -73,6 +73,12 @@ class RpcRouter:
|
|
|
73
73
|
self.connections = connections
|
|
74
74
|
self.kernel_server = kernel_server # Direct reference to KernelServer
|
|
75
75
|
|
|
76
|
+
# RPC 统计计数器
|
|
77
|
+
self._rpc_total = 0 # 总 RPC 调用次数
|
|
78
|
+
self._rpc_builtin = 0 # 内置方法调用次数
|
|
79
|
+
self._rpc_forwarded = 0 # 转发调用次数
|
|
80
|
+
self._rpc_errors = 0 # 错误次数
|
|
81
|
+
|
|
76
82
|
# Builtin method dispatch table
|
|
77
83
|
self.methods: dict[str, callable] = {
|
|
78
84
|
"registry.register": self._registry_register,
|
|
@@ -89,7 +95,6 @@ class RpcRouter:
|
|
|
89
95
|
"kernel.health": self._kernel_health,
|
|
90
96
|
"kernel.generate_tokens": self._kernel_generate_tokens,
|
|
91
97
|
"kernel.register_tokens": self._kernel_register_tokens,
|
|
92
|
-
"kernel.shutdown": self._kernel_shutdown,
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
# Pending cross-module forwards: internal_id -> PendingForward
|
|
@@ -102,6 +107,9 @@ class RpcRouter:
|
|
|
102
107
|
method = msg.get("method", "")
|
|
103
108
|
msg_id = msg.get("id")
|
|
104
109
|
|
|
110
|
+
# 统计:总调用次数
|
|
111
|
+
self._rpc_total += 1
|
|
112
|
+
|
|
105
113
|
# JSON-RPC Notification (no id) — currently not handled from clients
|
|
106
114
|
if msg_id is None:
|
|
107
115
|
return
|
|
@@ -111,12 +119,27 @@ class RpcRouter:
|
|
|
111
119
|
# Builtin method
|
|
112
120
|
handler = self.methods.get(method)
|
|
113
121
|
if handler:
|
|
122
|
+
# 统计:内置方法调用
|
|
123
|
+
self._rpc_builtin += 1
|
|
114
124
|
try:
|
|
115
125
|
result = await handler(caller_id, params)
|
|
116
|
-
await ws.send_text(_result_msg(msg_id, result))
|
|
117
126
|
except Exception as e:
|
|
127
|
+
# 统计:错误次数
|
|
128
|
+
self._rpc_errors += 1
|
|
118
129
|
print(f"[kernel] RPC handler error ({method}): {e}")
|
|
119
|
-
|
|
130
|
+
try:
|
|
131
|
+
await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
|
|
132
|
+
except Exception:
|
|
133
|
+
# Can't send error response, connection closed
|
|
134
|
+
pass
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Send result (may fail if connection closed during shutdown)
|
|
138
|
+
try:
|
|
139
|
+
await ws.send_text(_result_msg(msg_id, result))
|
|
140
|
+
except Exception as e:
|
|
141
|
+
# Connection closed during shutdown — this is normal, exit silently
|
|
142
|
+
pass
|
|
120
143
|
return
|
|
121
144
|
|
|
122
145
|
# Cross-module forward: method prefix is target module_id
|
|
@@ -126,19 +149,27 @@ class RpcRouter:
|
|
|
126
149
|
if target in self.connections:
|
|
127
150
|
# Check if target module is ready (state machine protection)
|
|
128
151
|
if not self.registry.is_ready(target):
|
|
152
|
+
# 统计:错误次数
|
|
153
|
+
self._rpc_errors += 1
|
|
129
154
|
await ws.send_text(_error_msg(
|
|
130
155
|
msg_id, MODULE_OFFLINE,
|
|
131
156
|
f"Module not ready: {target} (status: {self.registry.modules.get(target, {}).get('status', 'unknown')})"))
|
|
132
157
|
return
|
|
158
|
+
# 统计:转发调用
|
|
159
|
+
self._rpc_forwarded += 1
|
|
133
160
|
await self._forward(caller_id, ws, msg_id, target, method, params)
|
|
134
161
|
return
|
|
135
162
|
# Target not connected — check if registered but offline
|
|
136
163
|
if target in self.registry.modules:
|
|
164
|
+
# 统计:错误次数
|
|
165
|
+
self._rpc_errors += 1
|
|
137
166
|
await ws.send_text(_error_msg(
|
|
138
167
|
msg_id, MODULE_OFFLINE, f"Module offline: {target}"))
|
|
139
168
|
return
|
|
140
169
|
|
|
141
170
|
# Method not found
|
|
171
|
+
# 统计:错误次数
|
|
172
|
+
self._rpc_errors += 1
|
|
142
173
|
await ws.send_text(_error_msg(msg_id, METHOD_NOT_FOUND, f"Method not found: {method}"))
|
|
143
174
|
|
|
144
175
|
async def handle_response(self, module_id: str, msg: dict):
|
|
@@ -243,62 +274,102 @@ class RpcRouter:
|
|
|
243
274
|
async def _registry_register(self, caller_id: str, params: dict) -> dict:
|
|
244
275
|
mid = params.get("module_id")
|
|
245
276
|
if not mid:
|
|
246
|
-
|
|
277
|
+
raise ValueError("module_id required")
|
|
247
278
|
# Permission: only Launcher or the module itself
|
|
248
279
|
if caller_id != "launcher" and caller_id != mid:
|
|
249
|
-
|
|
280
|
+
raise PermissionError(f"Module '{caller_id}' cannot register as '{mid}'")
|
|
281
|
+
|
|
282
|
+
print(f"[kernel] registry.register called by {caller_id} for module {mid}")
|
|
283
|
+
print(f"[kernel] rpc_methods: {params.get('rpc_methods')}")
|
|
284
|
+
|
|
250
285
|
result = self.registry.register_module(params)
|
|
251
|
-
|
|
252
|
-
|
|
286
|
+
self.event_hub.publish_internal("module.registered", {"module_id": mid}, source=self.kernel_server.module_id)
|
|
287
|
+
|
|
288
|
+
# Only publish registry.updated if content actually changed
|
|
289
|
+
if result.get("changed", True):
|
|
290
|
+
from datetime import datetime, timezone
|
|
291
|
+
self.event_hub.publish_internal("registry.updated", {
|
|
292
|
+
"module_id": mid,
|
|
293
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
294
|
+
"action": "register",
|
|
295
|
+
}, source=self.kernel_server.module_id)
|
|
296
|
+
print(f"[kernel] registry.updated published for {mid}")
|
|
297
|
+
else:
|
|
298
|
+
print(f"[kernel] {mid} re-registered with no changes, skipping registry.updated")
|
|
299
|
+
|
|
300
|
+
# When Launcher registers, Kernel publishes its own module.ready
|
|
301
|
+
if mid == "launcher" and self.kernel_server:
|
|
302
|
+
self.kernel_server.publish_ready()
|
|
303
|
+
print(f"[kernel] launcher registered → kernel module.ready published")
|
|
253
304
|
|
|
254
|
-
# When Launcher registers, Kernel publishes its own module.ready
|
|
255
|
-
if mid == "launcher" and self.kernel_server:
|
|
256
|
-
self.kernel_server.publish_ready()
|
|
257
|
-
print(f"[kernel] launcher registered → kernel module.ready published")
|
|
258
305
|
return result
|
|
259
306
|
|
|
260
307
|
async def _registry_deregister(self, caller_id: str, params: dict) -> dict:
|
|
261
308
|
mid = params.get("module_id")
|
|
262
309
|
if not mid:
|
|
263
|
-
|
|
310
|
+
raise ValueError("module_id required")
|
|
264
311
|
if caller_id != "launcher" and caller_id != mid:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
312
|
+
raise PermissionError(f"Module '{caller_id}' cannot deregister '{mid}'")
|
|
313
|
+
|
|
314
|
+
self.registry.deregister_module(mid)
|
|
315
|
+
self.event_hub.publish_internal("module.unregistered", {"module_id": mid}, source=self.kernel_server.module_id)
|
|
316
|
+
|
|
317
|
+
# Publish registry.updated event for cache invalidation
|
|
318
|
+
from datetime import datetime, timezone
|
|
319
|
+
self.event_hub.publish_internal("registry.updated", {
|
|
320
|
+
"module_id": mid,
|
|
321
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
322
|
+
"action": "deregister",
|
|
323
|
+
}, source=self.kernel_server.module_id)
|
|
324
|
+
|
|
325
|
+
return {}
|
|
270
326
|
|
|
271
327
|
async def _registry_heartbeat(self, caller_id: str, params: dict) -> dict:
|
|
272
328
|
mid = params.get("module_id")
|
|
273
329
|
if not mid:
|
|
274
|
-
|
|
330
|
+
raise ValueError("module_id required")
|
|
275
331
|
if caller_id != "launcher" and caller_id != mid:
|
|
276
|
-
|
|
332
|
+
raise PermissionError(f"Module '{caller_id}' cannot heartbeat for '{mid}'")
|
|
333
|
+
|
|
277
334
|
return self.registry.heartbeat(mid)
|
|
278
335
|
|
|
279
336
|
async def _registry_lookup(self, caller_id: str, params: dict) -> dict:
|
|
337
|
+
field = params.get("field")
|
|
338
|
+
module = params.get("module")
|
|
339
|
+
value = params.get("value")
|
|
340
|
+
|
|
341
|
+
print(f"[kernel] registry.lookup called by {caller_id}: field={field}, module={module}, value={value}")
|
|
342
|
+
|
|
280
343
|
results = self.registry.lookup(
|
|
281
|
-
field=
|
|
282
|
-
module=
|
|
283
|
-
value=
|
|
344
|
+
field=field,
|
|
345
|
+
module=module,
|
|
346
|
+
value=value,
|
|
284
347
|
)
|
|
285
|
-
|
|
348
|
+
|
|
349
|
+
print(f"[kernel] registry.lookup results: {len(results)} matches")
|
|
350
|
+
for r in results:
|
|
351
|
+
print(f"[kernel] - {r['module']}.{r['field']} = {r['value']}")
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
"results": results,
|
|
355
|
+
"last_update_time": self.registry.last_update_time
|
|
356
|
+
}
|
|
286
357
|
|
|
287
358
|
async def _registry_get(self, caller_id: str, params: dict) -> dict:
|
|
288
359
|
path = params.get("path", "")
|
|
289
360
|
if not path:
|
|
290
|
-
|
|
361
|
+
raise ValueError("path required")
|
|
291
362
|
val, found = self.registry.get_by_path(path)
|
|
292
363
|
if not found:
|
|
293
|
-
|
|
294
|
-
return {"
|
|
364
|
+
raise KeyError(f"Path not found: {path}")
|
|
365
|
+
return {"value": val}
|
|
295
366
|
|
|
296
367
|
async def _registry_verify(self, caller_id: str, params: dict) -> dict:
|
|
297
368
|
token = params.get("token", "")
|
|
298
369
|
module_id = self.registry.verify_token(token)
|
|
299
|
-
if module_id:
|
|
300
|
-
|
|
301
|
-
return {"
|
|
370
|
+
if not module_id:
|
|
371
|
+
raise PermissionError("Invalid token")
|
|
372
|
+
return {"module_id": module_id}
|
|
302
373
|
|
|
303
374
|
# ── Builtin handlers: event.* ──
|
|
304
375
|
|
|
@@ -318,16 +389,15 @@ class RpcRouter:
|
|
|
318
389
|
async def _event_subscribe(self, caller_id: str, params: dict) -> dict:
|
|
319
390
|
events = params.get("events", [])
|
|
320
391
|
if not isinstance(events, list) or not events:
|
|
321
|
-
|
|
392
|
+
raise ValueError("events must be a non-empty list")
|
|
322
393
|
self.event_hub.handle_subscribe(caller_id, events)
|
|
323
|
-
return {
|
|
394
|
+
return {}
|
|
324
395
|
|
|
325
396
|
async def _event_unsubscribe(self, caller_id: str, params: dict) -> dict:
|
|
326
397
|
events = params.get("events", [])
|
|
327
398
|
if not isinstance(events, list) or not events:
|
|
328
|
-
|
|
329
|
-
self.event_hub.handle_unsubscribe(caller_id, events)
|
|
330
|
-
return {"ok": True}
|
|
399
|
+
raise ValueError("events must be a non-empty list")
|
|
400
|
+
return self.event_hub.handle_unsubscribe(caller_id, events)
|
|
331
401
|
|
|
332
402
|
# ── Builtin handlers: kernel.* ──
|
|
333
403
|
|
|
@@ -335,7 +405,16 @@ class RpcRouter:
|
|
|
335
405
|
return {"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
336
406
|
|
|
337
407
|
async def _kernel_stats(self, caller_id: str, params: dict) -> dict:
|
|
338
|
-
|
|
408
|
+
event_stats = self.event_hub.get_stats()
|
|
409
|
+
return {
|
|
410
|
+
**event_stats,
|
|
411
|
+
"rpc": {
|
|
412
|
+
"total": self._rpc_total,
|
|
413
|
+
"builtin": self._rpc_builtin,
|
|
414
|
+
"forwarded": self._rpc_forwarded,
|
|
415
|
+
"errors": self._rpc_errors
|
|
416
|
+
}
|
|
417
|
+
}
|
|
339
418
|
|
|
340
419
|
async def _kernel_health(self, caller_id: str, params: dict) -> dict:
|
|
341
420
|
eh_health = self.event_hub.get_health()
|
|
@@ -356,15 +435,15 @@ class RpcRouter:
|
|
|
356
435
|
params: {"modules": ["mod1", "mod2", ...]}
|
|
357
436
|
|
|
358
437
|
Returns:
|
|
359
|
-
{"
|
|
438
|
+
{"tokens": {"mod1": "token1", "mod2": "token2", ...}}
|
|
360
439
|
"""
|
|
361
440
|
# Only Launcher may request token generation
|
|
362
441
|
if caller_id != "launcher":
|
|
363
|
-
|
|
442
|
+
raise PermissionError("Only Launcher may generate tokens")
|
|
364
443
|
|
|
365
444
|
modules = params.get("modules", [])
|
|
366
445
|
if not isinstance(modules, list):
|
|
367
|
-
|
|
446
|
+
raise ValueError("modules must be a list")
|
|
368
447
|
|
|
369
448
|
import secrets
|
|
370
449
|
tokens = {}
|
|
@@ -374,25 +453,13 @@ class RpcRouter:
|
|
|
374
453
|
# Register tokens in registry
|
|
375
454
|
self.registry.register_tokens(tokens)
|
|
376
455
|
|
|
377
|
-
return {"
|
|
456
|
+
return {"tokens": tokens}
|
|
378
457
|
|
|
379
458
|
async def _kernel_register_tokens(self, caller_id: str, params: dict) -> dict:
|
|
380
459
|
# Only Launcher may register tokens
|
|
381
460
|
if caller_id != "launcher":
|
|
382
|
-
|
|
461
|
+
raise PermissionError("Only Launcher may register tokens")
|
|
383
462
|
self.registry.register_tokens(params)
|
|
384
|
-
return {
|
|
385
|
-
|
|
386
|
-
async def _kernel_shutdown(self, caller_id: str, params: dict) -> dict:
|
|
387
|
-
"""Shutdown Kernel. Only Launcher may call this."""
|
|
388
|
-
if caller_id != "launcher":
|
|
389
|
-
return {"ok": False, "error": "Only Launcher may shutdown Kernel"}
|
|
390
|
-
|
|
391
|
-
print("[kernel] Received shutdown request from Launcher")
|
|
392
|
-
|
|
393
|
-
# Schedule shutdown (don't block RPC response)
|
|
394
|
-
if self.kernel_server:
|
|
395
|
-
asyncio.create_task(self.kernel_server.shutdown())
|
|
463
|
+
return {}
|
|
396
464
|
|
|
397
|
-
return {"ok": True}
|
|
398
465
|
|