Coverage for node / src / stigmem_node / cli / plugins.py: 75%
167 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Plugin inspection CLI handlers."""
3from __future__ import annotations
5import argparse
6import os
7from typing import Any
9PLUGIN_DOCS_URL = "https://docs.stigmem.dev/plugins"
11KNOWN_PLUGINS: tuple[dict[str, str], ...] = (
12 {
13 "slug": "lazy-instruction-discovery",
14 "package": "stigmem-plugin-lazy-instruction-discovery",
15 "env_var": "STIGMEM_LAZY_INSTRUCTION_DISCOVERY_ENABLED",
16 "summary": "Opt-in instruction manifest discovery and migration helpers.",
17 },
18 {
19 "slug": "time-travel",
20 "package": "stigmem-plugin-time-travel",
21 "env_var": "STIGMEM_TIME_TRAVEL_ENABLED",
22 "summary": "Opt-in historical fact and recall query behavior.",
23 },
24 {
25 "slug": "tombstones",
26 "package": "stigmem-plugin-tombstones",
27 "env_var": "STIGMEM_TOMBSTONES_ENABLED",
28 "summary": "Opt-in right-to-be-forgotten tombstone enforcement.",
29 },
30 {
31 "slug": "memory-garden-acl",
32 "package": "stigmem-plugin-memory-garden-acl",
33 "env_var": "STIGMEM_MEMORY_GARDEN_ACL_ENABLED",
34 "summary": "Opt-in memory garden membership ACL filtering.",
35 },
36 {
37 "slug": "source-attestation",
38 "package": "stigmem-plugin-source-attestation",
39 "env_var": "STIGMEM_SOURCE_ATTESTATION_ENABLED",
40 "summary": "Opt-in source identity checks and source-trust recall signals.",
41 },
42 {
43 "slug": "multi-tenant",
44 "package": "stigmem-plugin-multi-tenant",
45 "env_var": "STIGMEM_MULTI_TENANT_ENABLED",
46 "summary": "Opt-in tenant scoping and default-tenant collapse.",
47 },
48)
51def _load_plugin_registry() -> Any:
52 from ..plugins import HookRegistry, register_discovered_plugins
54 registry = HookRegistry()
55 register_discovered_plugins(registry=registry)
56 registry.poll_plugin_health()
57 return registry
60def _plugin_report_by_name(registry: Any) -> dict[str, Any]:
61 return {report.plugin_name: report for report in registry.plugin_health_reports()}
64def _plugin_info_to_dict(info: Any, health: Any | None = None) -> dict[str, Any]:
65 known = _known_plugin(info.name)
66 data = {
67 "name": info.name,
68 "version": info.version,
69 "capabilities": list(info.capabilities),
70 "hooks": list(info.hooks),
71 "hook_count": len(info.hooks),
72 "depends_on": list(info.depends_on),
73 "discovery_source": info.discovery_source,
74 "signed_by": info.signed_by,
75 }
76 if known is not None: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 data["enabled"] = _env_enabled(known["env_var"])
78 data["enable_env_var"] = known["env_var"]
79 if health is not None: 79 ↛ 86line 79 didn't jump to line 86 because the condition on line 79 was always true
80 data["health"] = {
81 "status": health.status.value,
82 "message": health.message,
83 "checked_at": health.checked_at.isoformat(),
84 "error_summary": health.error_summary,
85 }
86 return data
89def _env_enabled(name: str) -> bool:
90 return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
93def _known_plugin(name: str) -> dict[str, str] | None:
94 normalized = name.removeprefix("stigmem-plugin-")
95 for plugin in KNOWN_PLUGINS:
96 if name in {plugin["slug"], plugin["package"]} or normalized == plugin["slug"]:
97 return plugin
98 return None
101def _installed_plugin_names(registry: Any) -> set[str]:
102 names: set[str] = set()
103 for info in registry.plugin_infos(): 103 ↛ 104line 103 didn't jump to line 104 because the loop on line 103 never started
104 names.add(str(info.name))
105 known = _known_plugin(str(info.name))
106 if known is not None:
107 names.add(known["slug"])
108 names.add(known["package"])
109 return names
112def _plugin_doctor_rows(registry: Any) -> list[dict[str, Any]]:
113 installed = _installed_plugin_names(registry)
114 rows: list[dict[str, Any]] = []
115 for plugin in KNOWN_PLUGINS:
116 is_installed = plugin["slug"] in installed or plugin["package"] in installed
117 is_enabled = _env_enabled(plugin["env_var"])
118 if is_enabled and not is_installed:
119 status = "enabled-not-installed"
120 recommendation = f"Install {plugin['package']} or unset {plugin['env_var']}."
121 elif is_installed and not is_enabled: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 status = "installed-disabled"
123 recommendation = f"Set {plugin['env_var']}=1 to enable it."
124 elif is_installed: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 status = "installed-enabled"
126 recommendation = "No action required."
127 else:
128 status = "not-installed"
129 recommendation = "No action required."
130 rows.append(
131 {
132 "slug": plugin["slug"],
133 "package": plugin["package"],
134 "env_var": plugin["env_var"],
135 "installed": is_installed,
136 "enabled": is_enabled,
137 "status": status,
138 "recommendation": recommendation,
139 }
140 )
141 return rows
144def _print_plugin_doctor_rows(rows: list[dict[str, Any]]) -> None:
145 print("Plugin state diagnostics:")
146 for row in rows:
147 print(
148 f"{row['package']} status={row['status']} "
149 f"enabled={str(row['enabled']).lower()} env={row['env_var']}"
150 )
151 if row["status"] in {"enabled-not-installed", "installed-disabled"}:
152 print(f" recommendation: {row['recommendation']}")
153 if all(row["status"] in {"installed-enabled", "not-installed"} for row in rows): 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true
154 print("Plugin configuration looks consistent.")
157def _cmd_plugins_list(args: argparse.Namespace) -> int:
158 import json
160 registry = _load_plugin_registry()
161 infos = registry.plugin_infos()
162 health_by_name = _plugin_report_by_name(registry)
163 rows = [_plugin_info_to_dict(info, health_by_name.get(info.name)) for info in infos]
164 if args.json:
165 print(json.dumps(rows, indent=2))
166 return 0
167 if not rows: 167 ↛ 170line 167 didn't jump to line 170 because the condition on line 167 was always true
168 print(f"No plugins registered. See {PLUGIN_DOCS_URL} for the plugin catalog.")
169 return 0
170 for row in rows:
171 health = row.get("health") or {}
172 health_status = health.get("status", "unknown")
173 enabled = row.get("enabled")
174 enabled_text = f" enabled={str(enabled).lower()}" if enabled is not None else ""
175 print(
176 f"{row['name']} {row['version']} "
177 f"hooks={row['hook_count']}{enabled_text} "
178 f"health={health_status} signed_by={row['signed_by']}"
179 )
180 return 0
183def _cmd_plugins_describe(args: argparse.Namespace) -> int:
184 import json
185 import sys
187 registry = _load_plugin_registry()
188 info = registry.plugin_info(args.name)
189 if info is None:
190 print(f"error: plugin not found: {args.name}", file=sys.stderr)
191 return 1
192 health = _plugin_report_by_name(registry).get(info.name)
193 data = _plugin_info_to_dict(info, health)
194 if args.json: 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true
195 print(json.dumps(data, indent=2))
196 return 0
197 print(f"name: {data['name']}")
198 print(f"version: {data['version']}")
199 print(f"signed_by: {data['signed_by']}")
200 print(f"capabilities: {', '.join(data['capabilities']) or '-'}")
201 print(f"hooks ({data['hook_count']}): {', '.join(data['hooks']) or '-'}")
202 print(f"depends_on: {', '.join(data['depends_on']) or '-'}")
203 print(f"discovery_source: {json.dumps(data['discovery_source'], sort_keys=True)}")
204 health_data = data.get("health") or {}
205 print(f"health: {health_data.get('status', 'unknown')}")
206 if health_data.get("message"): 206 ↛ 208line 206 didn't jump to line 208 because the condition on line 206 was always true
207 print(f"health_message: {health_data['message']}")
208 if health_data.get("error_summary"): 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true
209 print(f"health_error: {health_data['error_summary']}")
210 if health_data.get("checked_at"): 210 ↛ 212line 210 didn't jump to line 212 because the condition on line 210 was always true
211 print(f"health_checked_at: {health_data['checked_at']}")
212 return 0
215def _cmd_plugins_search(args: argparse.Namespace) -> int:
216 import json
218 query = args.query.strip().lower()
219 rows = [
220 plugin
221 for plugin in KNOWN_PLUGINS
222 if query in plugin["slug"]
223 or query in plugin["package"]
224 or query in plugin["summary"].lower()
225 ]
226 if args.json: 226 ↛ 227line 226 didn't jump to line 227 because the condition on line 226 was never true
227 print(json.dumps(rows, indent=2))
228 return 0
229 if not rows: 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true
230 print(f"No catalog matches for {args.query!r}. See {PLUGIN_DOCS_URL}.")
231 return 0
232 for row in rows:
233 print(f"{row['package']} - {row['summary']}")
234 print(f" install: python -m pip install '{row['package']}>=0.1.0,<2.0.0'")
235 print(f" enable: export {row['env_var']}=1")
236 return 0
239def _cmd_plugins_enable(args: argparse.Namespace) -> int:
240 plugin = _known_plugin(args.name)
241 if plugin is None: 241 ↛ 242line 241 didn't jump to line 242 because the condition on line 241 was never true
242 print(f"error: unknown plugin: {args.name}")
243 return 1
244 print(f"python -m pip install '{plugin['package']}>=0.1.0,<2.0.0'")
245 print(f"export {plugin['env_var']}=1")
246 print("Restart the stigmem node after changing plugin packages or gates.")
247 return 0
250def _cmd_plugins_disable(args: argparse.Namespace) -> int:
251 plugin = _known_plugin(args.name)
252 if plugin is None: 252 ↛ 253line 252 didn't jump to line 253 because the condition on line 252 was never true
253 print(f"error: unknown plugin: {args.name}")
254 return 1
255 print(f"unset {plugin['env_var']}")
256 print("Restart the stigmem node after changing plugin gates.")
257 return 0
260def _cmd_plugins_doctor(args: argparse.Namespace) -> int:
261 import json
263 registry = _load_plugin_registry()
264 rows = _plugin_doctor_rows(registry)
265 if args.json: 265 ↛ 266line 265 didn't jump to line 266 because the condition on line 265 was never true
266 print(json.dumps(rows, indent=2))
267 return 0
268 _print_plugin_doctor_rows(rows)
269 return 0
272def _cmd_doctor(args: argparse.Namespace) -> int:
273 import json
275 registry = _load_plugin_registry()
276 plugin_rows = _plugin_doctor_rows(registry)
277 payload = {
278 "status": "ok",
279 "plugins": plugin_rows,
280 }
281 if args.json: 281 ↛ 284line 281 didn't jump to line 284 because the condition on line 281 was always true
282 print(json.dumps(payload, indent=2))
283 return 0
284 print("status: ok")
285 _print_plugin_doctor_rows(plugin_rows)
286 return 0