@agentunion/kite 1.3.1 → 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 +287 -1
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -197
- 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 -197
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +408 -72
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +255 -71
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +344 -90
- package/extensions/services/watchdog/monitor.py +237 -21
- 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/server.py +445 -99
- package/extensions/services/web/static/css/style.css +138 -2
- package/extensions/services/web/static/index.html +295 -2
- package/extensions/services/web/static/js/app.js +1579 -5
- 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 +159 -16
- package/kernel/module.md +36 -33
- package/kernel/registry_store.py +70 -20
- package/kernel/rpc_router.py +134 -57
- package/kernel/server.py +292 -15
- 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/count_lines.py +34 -0
- package/launcher/entry.py +905 -166
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/launcher/process_manager.py +12 -1
- package/package.json +2 -1
- package/scripts/plan_manager.py +315 -0
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,58 +58,112 @@ 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
|
-
record["status"] = "
|
|
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
|
-
|
|
98
|
-
return {
|
|
135
|
+
# Don't change status — heartbeat just keeps alive, doesn't upgrade state
|
|
136
|
+
return {}
|
|
137
|
+
|
|
138
|
+
def set_connected(self, module_id: str):
|
|
139
|
+
"""Mark a module as connected (WS established, not yet registered).
|
|
140
|
+
Only upgrades from offline; doesn't downgrade from registered/ready."""
|
|
141
|
+
if module_id in self.modules:
|
|
142
|
+
if self.modules[module_id].get("status") == "offline":
|
|
143
|
+
self.modules[module_id]["status"] = "connected"
|
|
144
|
+
|
|
145
|
+
def set_ready(self, module_id: str):
|
|
146
|
+
"""Mark a module as ready (received module.ready event)."""
|
|
147
|
+
if module_id in self.modules:
|
|
148
|
+
self.modules[module_id]["status"] = "ready"
|
|
99
149
|
|
|
100
150
|
def set_offline(self, module_id: str):
|
|
101
|
-
"""Mark a module as offline (called on WebSocket disconnect)."""
|
|
151
|
+
"""Mark a module as offline (called after debounce on WebSocket disconnect)."""
|
|
102
152
|
if module_id in self.modules:
|
|
103
153
|
self.modules[module_id]["status"] = "offline"
|
|
104
154
|
|
|
155
|
+
def is_ready(self, module_id: str) -> bool:
|
|
156
|
+
"""Check if a module is in ready state."""
|
|
157
|
+
mod = self.modules.get(module_id)
|
|
158
|
+
return mod is not None and mod.get("status") == "ready"
|
|
159
|
+
|
|
105
160
|
def check_ttl(self) -> list[str]:
|
|
106
161
|
"""Mark modules as offline if heartbeat expired. Returns list of newly-offline module_ids."""
|
|
107
162
|
now = time.time()
|
|
108
163
|
expired = []
|
|
109
164
|
for mid, last in list(self.heartbeats.items()):
|
|
110
165
|
if mid in self.modules and now - last > self.ttl:
|
|
111
|
-
if self.modules[mid].get("status")
|
|
166
|
+
if self.modules[mid].get("status") not in ("offline",):
|
|
112
167
|
self.modules[mid]["status"] = "offline"
|
|
113
168
|
expired.append(mid)
|
|
114
169
|
return expired
|
|
@@ -160,17 +215,15 @@ class RegistryStore:
|
|
|
160
215
|
def lookup(self, field: str = None, module: str = None, value: str = None) -> list[dict]:
|
|
161
216
|
"""
|
|
162
217
|
Search across all online modules. All three params support glob patterns.
|
|
163
|
-
Returns list of {field, module,
|
|
218
|
+
Returns list of {field, module, value}.
|
|
164
219
|
"""
|
|
165
220
|
results = []
|
|
166
221
|
for mid, data in self.modules.items():
|
|
167
|
-
if data.get("status")
|
|
222
|
+
if data.get("status") not in ("registered", "ready"):
|
|
168
223
|
continue
|
|
169
224
|
if module and not fnmatch.fnmatch(mid, module):
|
|
170
225
|
continue
|
|
171
226
|
|
|
172
|
-
api_ep = data.get("api_endpoint", "")
|
|
173
|
-
|
|
174
227
|
if field:
|
|
175
228
|
matches = self._match_fields(data, field)
|
|
176
229
|
for fpath, fval in matches:
|
|
@@ -179,7 +232,6 @@ class RegistryStore:
|
|
|
179
232
|
results.append({
|
|
180
233
|
"field": fpath,
|
|
181
234
|
"module": mid,
|
|
182
|
-
"api_endpoint": api_ep,
|
|
183
235
|
"value": fval,
|
|
184
236
|
})
|
|
185
237
|
elif value:
|
|
@@ -188,14 +240,12 @@ class RegistryStore:
|
|
|
188
240
|
results.append({
|
|
189
241
|
"field": k,
|
|
190
242
|
"module": mid,
|
|
191
|
-
"api_endpoint": api_ep,
|
|
192
243
|
"value": v,
|
|
193
244
|
})
|
|
194
245
|
else:
|
|
195
246
|
results.append({
|
|
196
247
|
"field": "module_id",
|
|
197
248
|
"module": mid,
|
|
198
|
-
"api_endpoint": api_ep,
|
|
199
249
|
"value": mid,
|
|
200
250
|
})
|
|
201
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
|
|
@@ -124,15 +147,29 @@ class RpcRouter:
|
|
|
124
147
|
if dot_idx > 0:
|
|
125
148
|
target = method[:dot_idx]
|
|
126
149
|
if target in self.connections:
|
|
150
|
+
# Check if target module is ready (state machine protection)
|
|
151
|
+
if not self.registry.is_ready(target):
|
|
152
|
+
# 统计:错误次数
|
|
153
|
+
self._rpc_errors += 1
|
|
154
|
+
await ws.send_text(_error_msg(
|
|
155
|
+
msg_id, MODULE_OFFLINE,
|
|
156
|
+
f"Module not ready: {target} (status: {self.registry.modules.get(target, {}).get('status', 'unknown')})"))
|
|
157
|
+
return
|
|
158
|
+
# 统计:转发调用
|
|
159
|
+
self._rpc_forwarded += 1
|
|
127
160
|
await self._forward(caller_id, ws, msg_id, target, method, params)
|
|
128
161
|
return
|
|
129
162
|
# Target not connected — check if registered but offline
|
|
130
163
|
if target in self.registry.modules:
|
|
164
|
+
# 统计:错误次数
|
|
165
|
+
self._rpc_errors += 1
|
|
131
166
|
await ws.send_text(_error_msg(
|
|
132
167
|
msg_id, MODULE_OFFLINE, f"Module offline: {target}"))
|
|
133
168
|
return
|
|
134
169
|
|
|
135
170
|
# Method not found
|
|
171
|
+
# 统计:错误次数
|
|
172
|
+
self._rpc_errors += 1
|
|
136
173
|
await ws.send_text(_error_msg(msg_id, METHOD_NOT_FOUND, f"Method not found: {method}"))
|
|
137
174
|
|
|
138
175
|
async def handle_response(self, module_id: str, msg: dict):
|
|
@@ -237,64 +274,102 @@ class RpcRouter:
|
|
|
237
274
|
async def _registry_register(self, caller_id: str, params: dict) -> dict:
|
|
238
275
|
mid = params.get("module_id")
|
|
239
276
|
if not mid:
|
|
240
|
-
|
|
277
|
+
raise ValueError("module_id required")
|
|
241
278
|
# Permission: only Launcher or the module itself
|
|
242
279
|
if caller_id != "launcher" and caller_id != mid:
|
|
243
|
-
|
|
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
|
+
|
|
244
285
|
result = self.registry.register_module(params)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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")
|
|
304
|
+
|
|
254
305
|
return result
|
|
255
306
|
|
|
256
307
|
async def _registry_deregister(self, caller_id: str, params: dict) -> dict:
|
|
257
308
|
mid = params.get("module_id")
|
|
258
309
|
if not mid:
|
|
259
|
-
|
|
310
|
+
raise ValueError("module_id required")
|
|
260
311
|
if caller_id != "launcher" and caller_id != mid:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 {}
|
|
266
326
|
|
|
267
327
|
async def _registry_heartbeat(self, caller_id: str, params: dict) -> dict:
|
|
268
328
|
mid = params.get("module_id")
|
|
269
329
|
if not mid:
|
|
270
|
-
|
|
330
|
+
raise ValueError("module_id required")
|
|
271
331
|
if caller_id != "launcher" and caller_id != mid:
|
|
272
|
-
|
|
332
|
+
raise PermissionError(f"Module '{caller_id}' cannot heartbeat for '{mid}'")
|
|
333
|
+
|
|
273
334
|
return self.registry.heartbeat(mid)
|
|
274
335
|
|
|
275
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
|
+
|
|
276
343
|
results = self.registry.lookup(
|
|
277
|
-
field=
|
|
278
|
-
module=
|
|
279
|
-
value=
|
|
344
|
+
field=field,
|
|
345
|
+
module=module,
|
|
346
|
+
value=value,
|
|
280
347
|
)
|
|
281
|
-
|
|
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
|
+
}
|
|
282
357
|
|
|
283
358
|
async def _registry_get(self, caller_id: str, params: dict) -> dict:
|
|
284
359
|
path = params.get("path", "")
|
|
285
360
|
if not path:
|
|
286
|
-
|
|
361
|
+
raise ValueError("path required")
|
|
287
362
|
val, found = self.registry.get_by_path(path)
|
|
288
363
|
if not found:
|
|
289
|
-
|
|
290
|
-
return {"
|
|
364
|
+
raise KeyError(f"Path not found: {path}")
|
|
365
|
+
return {"value": val}
|
|
291
366
|
|
|
292
367
|
async def _registry_verify(self, caller_id: str, params: dict) -> dict:
|
|
293
368
|
token = params.get("token", "")
|
|
294
369
|
module_id = self.registry.verify_token(token)
|
|
295
|
-
if module_id:
|
|
296
|
-
|
|
297
|
-
return {"
|
|
370
|
+
if not module_id:
|
|
371
|
+
raise PermissionError("Invalid token")
|
|
372
|
+
return {"module_id": module_id}
|
|
298
373
|
|
|
299
374
|
# ── Builtin handlers: event.* ──
|
|
300
375
|
|
|
@@ -303,21 +378,26 @@ class RpcRouter:
|
|
|
303
378
|
event_type = params.get("event", "")
|
|
304
379
|
data = params.get("data")
|
|
305
380
|
echo = params.get("echo", False)
|
|
381
|
+
|
|
382
|
+
# When a module publishes module.ready, update its status in registry
|
|
383
|
+
if event_type == "module.ready":
|
|
384
|
+
mid = (data or {}).get("module_id", caller_id)
|
|
385
|
+
self.registry.set_ready(mid)
|
|
386
|
+
|
|
306
387
|
return self.event_hub.publish_event(caller_id, event_id, event_type, data, echo)
|
|
307
388
|
|
|
308
389
|
async def _event_subscribe(self, caller_id: str, params: dict) -> dict:
|
|
309
390
|
events = params.get("events", [])
|
|
310
391
|
if not isinstance(events, list) or not events:
|
|
311
|
-
|
|
392
|
+
raise ValueError("events must be a non-empty list")
|
|
312
393
|
self.event_hub.handle_subscribe(caller_id, events)
|
|
313
|
-
return {
|
|
394
|
+
return {}
|
|
314
395
|
|
|
315
396
|
async def _event_unsubscribe(self, caller_id: str, params: dict) -> dict:
|
|
316
397
|
events = params.get("events", [])
|
|
317
398
|
if not isinstance(events, list) or not events:
|
|
318
|
-
|
|
319
|
-
self.event_hub.handle_unsubscribe(caller_id, events)
|
|
320
|
-
return {"ok": True}
|
|
399
|
+
raise ValueError("events must be a non-empty list")
|
|
400
|
+
return self.event_hub.handle_unsubscribe(caller_id, events)
|
|
321
401
|
|
|
322
402
|
# ── Builtin handlers: kernel.* ──
|
|
323
403
|
|
|
@@ -325,7 +405,16 @@ class RpcRouter:
|
|
|
325
405
|
return {"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
326
406
|
|
|
327
407
|
async def _kernel_stats(self, caller_id: str, params: dict) -> dict:
|
|
328
|
-
|
|
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
|
+
}
|
|
329
418
|
|
|
330
419
|
async def _kernel_health(self, caller_id: str, params: dict) -> dict:
|
|
331
420
|
eh_health = self.event_hub.get_health()
|
|
@@ -334,7 +423,7 @@ class RpcRouter:
|
|
|
334
423
|
"module_count": len(self.registry.modules),
|
|
335
424
|
"online_count": sum(
|
|
336
425
|
1 for m in self.registry.modules.values()
|
|
337
|
-
if m.get("status")
|
|
426
|
+
if m.get("status") in ("registered", "ready")
|
|
338
427
|
),
|
|
339
428
|
"event_stats": eh_health.get("details", {}),
|
|
340
429
|
}
|
|
@@ -346,15 +435,15 @@ class RpcRouter:
|
|
|
346
435
|
params: {"modules": ["mod1", "mod2", ...]}
|
|
347
436
|
|
|
348
437
|
Returns:
|
|
349
|
-
{"
|
|
438
|
+
{"tokens": {"mod1": "token1", "mod2": "token2", ...}}
|
|
350
439
|
"""
|
|
351
440
|
# Only Launcher may request token generation
|
|
352
441
|
if caller_id != "launcher":
|
|
353
|
-
|
|
442
|
+
raise PermissionError("Only Launcher may generate tokens")
|
|
354
443
|
|
|
355
444
|
modules = params.get("modules", [])
|
|
356
445
|
if not isinstance(modules, list):
|
|
357
|
-
|
|
446
|
+
raise ValueError("modules must be a list")
|
|
358
447
|
|
|
359
448
|
import secrets
|
|
360
449
|
tokens = {}
|
|
@@ -364,25 +453,13 @@ class RpcRouter:
|
|
|
364
453
|
# Register tokens in registry
|
|
365
454
|
self.registry.register_tokens(tokens)
|
|
366
455
|
|
|
367
|
-
return {"
|
|
456
|
+
return {"tokens": tokens}
|
|
368
457
|
|
|
369
458
|
async def _kernel_register_tokens(self, caller_id: str, params: dict) -> dict:
|
|
370
459
|
# Only Launcher may register tokens
|
|
371
460
|
if caller_id != "launcher":
|
|
372
|
-
|
|
461
|
+
raise PermissionError("Only Launcher may register tokens")
|
|
373
462
|
self.registry.register_tokens(params)
|
|
374
|
-
return {
|
|
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())
|
|
463
|
+
return {}
|
|
386
464
|
|
|
387
|
-
return {"ok": True}
|
|
388
465
|
|