From c89eee0e666c69e87e61f62ea3c204e494cdbe1a Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Wed, 19 Aug 2026 19:57:23 +0800 Subject: [PATCH] Retire generated audit and autonomy stack --- backend/app/api/chat.py | 2 +- backend/app/api/dev_layer_api.py | 11 +- backend/app/api/routes.py | 22 - backend/app/mcp/mcp_bridge/index.ts | 46 -- .../app/services/autonomy/autonomy_engine.py | 202 ----- backend/app/skills/builtin/skill_audit.py | 365 --------- .../skills/builtin/skill_ecosystem_audit.py | 757 ------------------ .../app/skills/builtin/skill_ucore_index.py | 502 ------------ backend/app/skills/catalogue.json | 3 - backend/health/autonomy_engine.py | 201 ----- backend/tests/test_dev_layer_quick_actions.py | 13 + docs/PLATES_SYSTEM_SPEC.md | 7 +- docs/SKILLS_AUDIT_2026-08-18.md | 6 +- .../src/skills/organisms/DevHudPanel.vue | 18 +- 14 files changed, 38 insertions(+), 2117 deletions(-) delete mode 100644 backend/app/services/autonomy/autonomy_engine.py delete mode 100644 backend/app/skills/builtin/skill_audit.py delete mode 100644 backend/app/skills/builtin/skill_ecosystem_audit.py delete mode 100644 backend/app/skills/builtin/skill_ucore_index.py delete mode 100644 backend/health/autonomy_engine.py create mode 100644 backend/tests/test_dev_layer_quick_actions.py diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py index 81f2b70b..f885a78e 100644 --- a/backend/app/api/chat.py +++ b/backend/app/api/chat.py @@ -312,7 +312,7 @@ async def _execute_tool(tool_name: str, arguments: dict) -> str: return json.dumps({"skills": skills[:20], "total": len(skills)}) except (ImportError, AttributeError): return json.dumps({"skills": [ - {"id": "ecosystem-audit", "name": "Ecosystem Audit", "category": "system"}, + {"id": "backup", "name": "Backup", "category": "maintenance"}, {"id": "vault_discovery", "name": "Vault Discovery", "category": "knowledge"}, {"id": "workflow_audit", "name": "Workflow Audit", "category": "workflow"}, ], "total": 3, "source": "samples"}) diff --git a/backend/app/api/dev_layer_api.py b/backend/app/api/dev_layer_api.py index c2d2e16e..16b2c45c 100644 --- a/backend/app/api/dev_layer_api.py +++ b/backend/app/api/dev_layer_api.py @@ -169,13 +169,10 @@ def _get_capabilities_hud() -> dict: _QUICK_ACTIONS: list[dict] = [ - {"id": "ecosystem-audit", "label": "Run Ecosystem Audit", "icon": "monitoring"}, - {"id": "system-health", "label": "System Health Check", "icon": "favorite"}, - {"id": "binder-refresh", "label": "Refresh Binder Context", "icon": "folder"}, - {"id": "spool-prune", "label": "Prune Spool Logs", "icon": "cleaning_services"}, - {"id": "vault-sync", "label": "Sync Vault", "icon": "sync"}, - {"id": "tasker-sync", "label": "Sync Tasker", "icon": "assignment"}, - {"id": "snapshot", "label": "Take Snapshot", "icon": "camera"}, + { + "id": "system-health", "label": "System Health Check", + "icon": "favorite", "method": "GET", "path": "/api/health/full", + }, ] diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 5d0c6e3c..19c0fe95 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -359,28 +359,6 @@ def register_routes(app: web.Application) -> None: app.router.add_patch("/api/binder/update", handle_binder_update) app.router.add_patch("/api/binder/score", handle_binder_score) - # ── Autonomy Engine ──────────────────────────────────────────── - try: - import json - from pathlib import Path - - from aiohttp import web - - from app.core.settings import settings - - STATE_FILE = settings.logs_dir / "autonomy_state.json" - - async def handle_autonomy_state(_request: web.Request) -> web.Response: - """GET /api/autonomy/state — return last autonomy check state.""" - if STATE_FILE.exists(): - return web.json_response(json.loads(STATE_FILE.read_text())) - return web.json_response({"healthy": False, "error": "No autonomy data yet"}) - - app.router.add_get("/api/autonomy/state", handle_autonomy_state) - log.debug("Autonomy state endpoint registered") - except Exception as e: - log.debug("Autonomy engine not available: %s", e) - register_surface_routes(app) register_snack_routes(app) register_container_routes(app) diff --git a/backend/app/mcp/mcp_bridge/index.ts b/backend/app/mcp/mcp_bridge/index.ts index 3d213209..5ed33e0d 100644 --- a/backend/app/mcp/mcp_bridge/index.ts +++ b/backend/app/mcp/mcp_bridge/index.ts @@ -44,16 +44,6 @@ async function apiPost( // ─── Tool definitions ───────────────────────────────────────────── const TOOLS = [ - { - name: "ucore_ecosystem_audit", - description: - "Run a full uCore ecosystem health audit. Returns health percentage, working/broken/orphaned counts across all 54 skills, surfaces, MCP servers, and tests.", - inputSchema: { - type: "object", - properties: {}, - required: [], - }, - }, { name: "ucore_list_skills", description: @@ -146,16 +136,6 @@ const TOOLS = [ required: ["query"], }, }, - { - name: "ucore_autonomy_state", - description: - "Get the latest autonomy engine health state (from the overnight cron). Includes overall health %, Ollama status, and last audit timestamp.", - inputSchema: { - type: "object", - properties: {}, - required: [], - }, - }, { name: "ucore_list_secrets", description: @@ -252,20 +232,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (name) { - case "ucore_ecosystem_audit": { - const data = await apiPost("/api/skills/ecosystem-audit/run", { - action: "assess", - }); - return { - content: [ - { - type: "text", - text: JSON.stringify(data, null, 2), - }, - ], - }; - } - case "ucore_list_skills": { const search = args?.search as string | undefined; let data = await apiGet("/api/skills"); @@ -360,18 +326,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } - case "ucore_autonomy_state": { - const data = await apiGet("/api/autonomy/state"); - return { - content: [ - { - type: "text", - text: JSON.stringify(data, null, 2), - }, - ], - }; - } - case "ucore_list_secrets": { const data = await apiGet("/api/secrets"); return { diff --git a/backend/app/services/autonomy/autonomy_engine.py b/backend/app/services/autonomy/autonomy_engine.py deleted file mode 100644 index a21c4097..00000000 --- a/backend/app/services/autonomy/autonomy_engine.py +++ /dev/null @@ -1,202 +0,0 @@ -"""Autonomy Engine — scheduled background tasks for uCore ecosystem. - -Runs every 24 hours (or on demand) to: -- Execute ecosystem-audit -- Check health thresholds -- Log results to ``$UDOS_HOME/logs`` -- Alert if health drops below 95% - -Usage as cron job: - */30 * * * * cd ~/Code/uCore && backend/.venv/bin/python -m health.autonomy_engine - -Usage as one-shot: - python -m health.autonomy_engine --once -""" - -from __future__ import annotations - -import json -import logging -import os -import sys -import time -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from app.core.settings import settings - -LOG_DIR = settings.logs_dir -LOG_DIR.mkdir(parents=True, exist_ok=True) - -AUDIT_LOG = LOG_DIR / "autonomy.log" -STATE_FILE = LOG_DIR / "autonomy_state.json" -HEALTH_THRESHOLD = 95.0 - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [autonomy] %(levelname)s %(message)s", - handlers=[ - logging.FileHandler(AUDIT_LOG), - logging.StreamHandler(sys.stdout), - ], -) -log = logging.getLogger("autonomy") - - -def _call_api( - path: str, method: str = "GET", body: dict | None = None, timeout: int = 120 -) -> dict | None: - """Call the uCore backend API.""" - import urllib.request - - url = f"http://127.0.0.1:8484{path}" - try: - data = None - if body is not None: - data = json.dumps(body).encode("utf-8") - req = urllib.request.Request( - url, - data=data, - headers={"Content-Type": "application/json"} if data else {}, - method=method, - ) - with urllib.request.urlopen(req, timeout=timeout) as resp: - return json.loads(resp.read().decode("utf-8")) - except Exception as e: - log.warning(f"API call failed: {path} — {e}") - return None - - -def run_ecosystem_audit() -> dict[str, Any]: - """Run the ecosystem-audit skill and return parsed results.""" - log.info("Starting ecosystem audit...") - start = time.time() - - result = _call_api( - "/api/skills/ecosystem-audit/run", - method="POST", - body={"action": "assess"}, - timeout=120, - ) - - elapsed = time.time() - start - - if result is None: - return { - "success": False, - "error": "API call failed", - "duration_seconds": round(elapsed, 1), - "timestamp": datetime.now(timezone.utc).isoformat(), - } - - health = result.get("health", {}) - return { - "success": result.get("success", False), - "health_pct": health.get("health_pct", 0), - "working": health.get("working", 0), - "untested": health.get("untested", 0), - "broken": health.get("broken", 0), - "orphaned": health.get("orphaned", 0), - "total_items": health.get("total_items", 0), - "duration_seconds": round(elapsed, 1), - "timestamp": datetime.now(timezone.utc).isoformat(), - } - - -def check_ollama() -> dict[str, Any]: - """Check Ollama status.""" - result = _call_api("/api/ollama/status", timeout=10) - if result is None: - return {"online": False, "error": "unreachable"} - return { - "online": result.get("online", False), - "model_count": result.get("model_count", 0), - } - - -def save_state(state: dict[str, Any]) -> None: - """Persist autonomy state for frontend consumption.""" - try: - STATE_FILE.write_text(json.dumps(state, indent=2, default=str)) - except Exception as e: - log.warning(f"Failed to save state: {e}") - - -def load_state() -> dict[str, Any]: - """Load last known autonomy state.""" - try: - if STATE_FILE.exists(): - return json.loads(STATE_FILE.read_text()) - except Exception: - pass - return {} - - -def run_full_check() -> dict[str, Any]: - """Run all health checks and return combined state.""" - log.info("=== Autonomy Engine: Full Health Check ===") - - audit = run_ecosystem_audit() - ollama = check_ollama() - - health_pct = audit.get("health_pct", 0) - - if audit.get("success"): - if health_pct >= HEALTH_THRESHOLD: - log.info(f"Health OK: {health_pct}% (threshold: {HEALTH_THRESHOLD}%)") - else: - log.warning( - f"Health BELOW threshold: {health_pct}% < {HEALTH_THRESHOLD}% " - f"({audit.get('broken', 0)} broken, {audit.get('untested', 0)} untested)" - ) - else: - log.error(f"Audit failed: {audit.get('error', 'unknown')}") - - if not ollama.get("online"): - log.warning("Ollama is offline") - - state = { - "last_audit": audit, - "ollama": ollama, - "health_pct": health_pct, - "healthy": health_pct >= HEALTH_THRESHOLD, - "threshold": HEALTH_THRESHOLD, - "updated_at": datetime.now(timezone.utc).isoformat(), - } - - save_state(state) - log.info( - f"State saved. Health: {health_pct}% | Ollama: {'online' if ollama.get('online') else 'offline'}" - ) - return state - - -def main() -> None: - """Entry point — run full check and exit.""" - state = run_full_check() - if not state.get("healthy"): - sys.exit(1) - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="uCore Autonomy Engine") - parser.add_argument("--once", action="store_true", help="Run once and exit") - parser.add_argument( - "--interval", type=int, default=86400, help="Seconds between checks (default: 24h)" - ) - args = parser.parse_args() - - if args.once: - state = run_full_check() - print(json.dumps(state, indent=2, default=str)) - else: - log.info(f"Starting autonomy loop (interval: {args.interval}s)") - while True: - try: - run_full_check() - except Exception as e: - log.error(f"Check failed: {e}") - time.sleep(args.interval) diff --git a/backend/app/skills/builtin/skill_audit.py b/backend/app/skills/builtin/skill_audit.py deleted file mode 100644 index 36c06f93..00000000 --- a/backend/app/skills/builtin/skill_audit.py +++ /dev/null @@ -1,365 +0,0 @@ -"""Skill Audit — smoke-test all skills with import + execution checks. - -Comprehensive audit that actually tries to load and execute each skill: -1. Discovers all skills in builtin/ -2. Attempts to import each skill module -3. Attempts to instantiate BaseSkill subclasses and call run(dry_run=True) -4. For module-level functions, attempts to call run() with safety wrapper -5. Reports failures with exception traces -6. Cross-references against skills audit status doc - -Integrates with the uCore skill registry via BaseSkill pattern. -""" -from __future__ import annotations - -import importlib -import importlib.util -import json -import logging -import sys -import time -import traceback -from pathlib import Path -from typing import Any - -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.audit") - -SKILLS_DIR = Path(__file__).resolve().parent -UCORE_ROOT = SKILLS_DIR.parent.parent.parent.parent -EXCLUDES = { - "__pycache__", "__archived__", ".mypy_cache", - "__init__", "base", "state", -} - - -def _discover_skill_modules() -> list[str]: - """Discover all skill modules (files + base-skill classes).""" - modules: list[str] = [] - for f in sorted(SKILLS_DIR.glob("*.py")): - name = f.stem - if name in EXCLUDES or name.startswith("."): - continue - modules.append(name) - return modules - - -def _safe_import_module(name: str) -> tuple[Any | None, str | None]: - """Try to import a skill module. Returns (module, error).""" - try: - spec = importlib.util.spec_from_file_location( - name, - SKILLS_DIR / f"{name}.py", - ) - if spec is None or spec.loader is None: - return None, "Could not create module spec" - mod = importlib.util.module_from_spec(spec) - sys.modules[name] = mod - spec.loader.exec_module(mod) - return mod, None - except Exception as exc: - return None, f"ImportError: {exc}" - - -def _find_base_skill_subclass(mod: Any) -> type | None: - """Find the first BaseSkill subclass in a module.""" - for attr_name in dir(mod): - attr = getattr(mod, attr_name) - if ( - isinstance(attr, type) - and issubclass(attr, BaseSkill) - and attr is not BaseSkill - ): - return attr - return None - - -def _has_module_run(mod: Any) -> bool: - """Check if module has a top-level async run() or def run().""" - return hasattr(mod, "run") and callable(mod.run) - - -async def _smoke_test_skill( - name: str, - mod: Any, - skill_cls: type | None, - timeout: float = 10.0, -) -> dict[str, Any]: - """Smoke-test a single skill: import, instantiate, execute.""" - result: dict[str, Any] = { - "name": name, - "file": f"{name}.py", - "import_status": "success", - "instantiate_status": "skipped", - "execute_status": "skipped", - "error": None, - "traceback": None, - "duration_ms": 0, - "is_base_skill": skill_cls is not None, - "has_module_run": False, - } - - if skill_cls is None and not _has_module_run(mod): - result["import_status"] = "warning" - result["error"] = "No BaseSkill subclass or run() function found" - return result - - t0 = time.perf_counter() - - # Instantiate BaseSkill - if skill_cls is not None: - try: - instance = skill_cls() - result["instantiate_status"] = "success" - except Exception as exc: - result["instantiate_status"] = "failed" - result["error"] = str(exc) - result["traceback"] = traceback.format_exc() - result["duration_ms"] = round((time.perf_counter() - t0) * 1000, 1) - return result - - # Execute with dry_run / smoke-test - try: - t1 = time.perf_counter() - if hasattr(instance, "run"): - out = await instance.run(dry_run=True, smoke_test=True, action="report") - else: - out = {"dry_run": True, "status": "no run method"} - result["execute_status"] = "success" - result["output"] = out.get("success", True) if isinstance(out, dict) else True - result["duration_ms"] = round((time.perf_counter() - t1) * 1000, 1) - except TypeError: - # Skill doesn't accept dry_run — try without - try: - t1 = time.perf_counter() - out = await instance.run() - result["execute_status"] = "success" - result["output"] = True - result["duration_ms"] = round((time.perf_counter() - t1) * 1000, 1) - except Exception as exc2: - result["execute_status"] = "failed" - result["error"] = str(exc2) - result["traceback"] = traceback.format_exc() - result["duration_ms"] = round((time.perf_counter() - t1) * 1000, 1) - except Exception as exc: - result["execute_status"] = "failed" - result["error"] = str(exc) - result["traceback"] = traceback.format_exc() - result["duration_ms"] = round((time.perf_counter() - t0) * 1000, 1) - else: - # Module-level run() function - result["has_module_run"] = True - try: - t1 = time.perf_counter() - if hasattr(mod.run, "__code__"): - # Check if it's async - import inspect - if inspect.iscoroutinefunction(mod.run): - out = await mod.run() - else: - out = mod.run() - result["execute_status"] = "success" - result["output"] = out.get("success", True) if isinstance(out, dict) else True - result["duration_ms"] = round((time.perf_counter() - t1) * 1000, 1) - else: - result["execute_status"] = "skipped" - result["error"] = "run() is not a function" - except Exception as exc: - result["execute_status"] = "failed" - result["error"] = str(exc) - result["traceback"] = traceback.format_exc() - result["duration_ms"] = round((time.perf_counter() - t1) * 1000, 1) - - result["duration_ms"] = round((time.perf_counter() - t0) * 1000, 1) - return result - - -def _classify_status(result: dict) -> str: - """Classify overall skill health.""" - if result.get("import_status") == "failed": - return "broken" - if result.get("execute_status") == "failed": - return "broken" - if result.get("import_status") == "warning": - return "untested" - if result.get("execute_status") == "success": - return "working" - if result.get("instantiate_status") == "failed": - return "broken" - return "untested" - - -class SkillAuditSkill(BaseSkill): - """Smoke-test all uCore builtin skills and report health.""" - - meta = SkillMeta( - id="skill-audit", - name="Skill Auditor (Smoke-Test)", - description=( - "Discover, import, instantiate, and execute all builtin skills." - " Reports health status: working, untested, or broken." - ), - category="developer", - params=[ - SkillParam( - name="action", - type="string", - description="'audit' (full smoke-test), 'discover' (list only), or 'report' (last audit)", - required=False, - default="audit", - ), - SkillParam( - name="target", - type="string", - description="Specific skill name to test (omit for all)", - required=False, - default="", - ), - SkillParam( - name="timeout", - type="integer", - description="Per-skill timeout in seconds", - required=False, - default=10, - ), - ], - timeout=120, - requires_confirmation=False, - ) - - async def run(self, **kwargs) -> dict: - action = kwargs.get("action", "audit") - target = kwargs.get("target", "") - timeout = int(kwargs.get("timeout", 10)) - - if action == "discover": - modules = _discover_skill_modules() - return { - "success": True, - "action": "discover", - "count": len(modules), - "skills": modules, - } - - if action == "report": - return self._load_last_report() - - # Full audit / smoke-test - modules = _discover_skill_modules() - if target: - modules = [m for m in modules if target in m] - if not modules: - return { - "success": False, - "error": f"No skills match target '{target}'", - } - - results: list[dict] = [] - working = untested = broken = 0 - start_time = time.perf_counter() - - for name in modules: - mod, import_err = _safe_import_module(name) - if import_err: - results.append({ - "name": name, - "file": f"{name}.py", - "import_status": "failed", - "execute_status": "skipped", - "error": import_err, - "is_base_skill": False, - "duration_ms": 0, - }) - broken += 1 - continue - - skill_cls = _find_base_skill_subclass(mod) - result = await _smoke_test_skill(name, mod, skill_cls, timeout) - results.append(result) - - status = _classify_status(result) - if status == "working": - working += 1 - elif status == "broken": - broken += 1 - else: - untested += 1 - - total_duration = round((time.perf_counter() - start_time) * 1000, 1) - - report = { - "timestamp": int(time.time()), - "total_skills": len(modules), - "working": working, - "untested": untested, - "broken": broken, - "health_pct": round((working / len(modules)) * 100, 1) if modules else 0, - "duration_ms": total_duration, - "skills": results, - "recommendations": _generate_recommendations(working, untested, broken, results), - } - - self._persist_report(report) - return {"success": True, "action": "audit", "report": report} - - def _persist_report(self, report: dict) -> None: - """Save report to seeds/ for frontend consumption.""" - try: - seeds = UCORE_ROOT / "seeds" - seeds.mkdir(exist_ok=True) - out = seeds / "skill-audit-report.json" - out.write_text(json.dumps(report, indent=2, default=str)) - except Exception: - pass - - def _load_last_report(self) -> dict: - """Load the last saved audit report.""" - report_file = UCORE_ROOT / "seeds" / "skill-audit-report.json" - if report_file.exists(): - try: - return {"success": True, "action": "report", "report": json.loads(report_file.read_text())} - except Exception: - pass - return {"success": False, "error": "No previous audit report found"} - - -def _generate_recommendations( - working: int, - untested: int, - broken: int, - results: list[dict], -) -> list[str]: - """Generate actionable recommendations.""" - recs = [] - - if broken > 0: - names = [r["name"] for r in results if _classify_status(r) == "broken"] - recs.append( - f"Fix {broken} broken skill(s): {', '.join(names[:5])}" - + (f" +{len(names) - 5} more" if len(names) > 5 else "") - ) - - if untested > 0: - names = [r["name"] for r in results if _classify_status(r) == "untested"] - recs.append( - f"Verify {untested} untested skill(s): {', '.join(names[:5])}" - + (f" +{len(names) - 5} more" if len(names) > 5 else "") - ) - - if working == 0 and broken == 0: - recs.append("No skills were tested — verify skill directory path") - - if working > 0: - recs.append(f"{working} skills working correctly — no action needed") - - # Check for skills with no BaseSkill subclass (skip the module-level ones) - no_base = [r["name"] for r in results if not r.get("is_base_skill") and r.get("import_status") != "failed"] - if no_base: - recs.append( - f"{len(no_base)} skill(s) without BaseSkill subclass: {', '.join(no_base[:5])}" - + (f" +{len(no_base) - 5} more" if len(no_base) > 5 else "") - + " — consider converting to BaseSkill pattern" - ) - - return recs diff --git a/backend/app/skills/builtin/skill_ecosystem_audit.py b/backend/app/skills/builtin/skill_ecosystem_audit.py deleted file mode 100644 index 0afeb27a..00000000 --- a/backend/app/skills/builtin/skill_ecosystem_audit.py +++ /dev/null @@ -1,757 +0,0 @@ -"""Ecosystem Audit Skill — comprehensive inventory of uCore ecosystem. - -Discovers and catalogues: - - Skills (name, file, category, description, params from SkillMeta) - - Paths (file system paths used by skills, configs, vault, seeds) - - Variables (scope, key, type, default from variable APIs) - - Secrets (key, store, scope from config/env files) - - MCP servers and bridge components owned by uCore - - Routes (method, path, handler from routes.py) - - Runtimes (name, file, endpoints, variables, commands from backend modules) - -Generates seeds/ecosystem-registry.json for frontend consumption. -""" - -from __future__ import annotations - -import json -import logging -import re -from pathlib import Path - -from app.core.settings import settings -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.ecosystem_audit") - -ROOT_DIR = Path(__file__).parent.parent.parent.parent.parent -BACKEND_DIR = ROOT_DIR / "backend" / "app" -SKILLS_DIR = BACKEND_DIR / "skills" / "builtin" -UCODE_DIR = ROOT_DIR.parent / "uCode" -HOMENEST_DIR = ROOT_DIR.parent / "HomeNest" -REPO_MAP = { - str(ROOT_DIR): "uCore", - str(ROOT_DIR.parent / "uCode"): "uCode", - str(ROOT_DIR.parent / "HomeNest"): "HomeNest", -} - - -def _repo_for(path: Path) -> str: - """Determine which repo a path belongs to.""" - sp = str(path) - for root, name in REPO_MAP.items(): - if sp.startswith(root): - return name - return "unknown" - - -API_DIR = BACKEND_DIR / "api" -ROUTES_FILE = API_DIR / "routes.py" -ENV_FILE = ROOT_DIR.parent.parent / ".config" / "hivemind" / ".env" -VAULT_CONFIG = ROOT_DIR.parent / "uCode" / "config" / "vault.yaml" -SECRETS_API = API_DIR / "secret_store_api.py" -VARIABLES_API = API_DIR / "variables_api.py" -SEEDS_DIR = ROOT_DIR / "seeds" -OUTPUT_FILE = SEEDS_DIR / "ecosystem-registry.json" - -EXCLUDES = {"__pycache__", "__archived__", ".mypy_cache", "__init__"} - - -class EcosystemAuditSkill(BaseSkill): - meta = SkillMeta( - id="ecosystem-audit", - name="Ecosystem Auditor v1", - description=( - "Comprehensive ecosystem audit: skills, paths," - " variables, secrets, MCP servers, routes, runtimes." - " Generates ecosystem-registry.json." - ), - category="developer", - params=[ - SkillParam( - name="action", - type="string", - description=( - "Action: 'audit-skills', 'audit-routes'," - " 'audit-secrets', 'audit-variables'," - " 'audit-mcp', 'audit-paths'," - " 'audit-runtimes', 'report', 'generate'" - ), - required=True, - ), - SkillParam( - name="output", - type="string", - description="Output path for generated JSON", - required=False, - ), - ], - timeout=180, - requires_confirmation=False, - ) - - async def run(self, **kwargs) -> dict: - action = kwargs.get("action", "report") - output = kwargs.get("output", str(OUTPUT_FILE)) - - actions = { - "audit-skills": lambda: self._audit_skills(), - "audit-routes": lambda: self._audit_routes(), - "audit-secrets": lambda: self._audit_secrets(), - "audit-variables": lambda: self._audit_variables(), - "audit-mcp": lambda: self._audit_mcp(), - "audit-paths": lambda: self._audit_paths(), - "audit-runtimes": lambda: self._audit_runtimes(), - "assess": lambda: self._assess(output), - "report": lambda: self._report(), - "generate": lambda: self._generate(output), - } - - handler = actions.get(action) - if handler is None: - return {"success": False, "error": f"Unknown action: {action}"} - return handler() - - # ─── Audit Skills ──────────────────────────────────────────────── - - def _audit_skills(self) -> dict: - """Discover all builtin skills with metadata.""" - skills = [] - if not SKILLS_DIR.exists(): - return {"success": True, "skills": []} - - for f in sorted(SKILLS_DIR.glob("*.py")): - if f.name.startswith("_") or f.name in EXCLUDES: - continue - - skill_info = self._parse_skill_file(f) - if skill_info: - skills.append(skill_info) - - return {"success": True, "skills": skills, "total": len(skills)} - - def _parse_skill_file(self, filepath: Path) -> dict | None: - """Extract SkillMeta from a skill Python file.""" - try: - content = filepath.read_text() - except Exception: - return None - - info: dict = { - "file": str(filepath.relative_to(ROOT_DIR)), - "name": filepath.stem.replace("skill_", "").replace("_", " "), - "skill_id": "", - "category": "unknown", - "description": "", - "params": [], - "timeout": 0, - "requires_confirmation": False, - } - - # Extract skill id from SkillMeta - id_match = re.search(r'id\s*=\s*"([^"]+)"', content) - if id_match: - info["skill_id"] = id_match.group(1) - - # Extract name - name_match = re.search(r'name\s*=\s*"([^"]+)"', content) - if name_match: - info["name"] = name_match.group(1) - - # Extract description - desc_match = re.search( - r"description\s*=\s*\(\s*\n?(.*?)\n?\s*\)", - content, - re.DOTALL, - ) - if desc_match: - desc = desc_match.group(1).strip().strip('"').strip("'") - parts = desc.split() - desc = " ".join([d.strip().strip('"').strip("'") for d in parts]) - info["description"] = desc[:200] - - # Extract category - cat_match = re.search(r'category\s*=\s*"([^"]+)"', content) - if cat_match: - info["category"] = cat_match.group(1) - - # Extract timeout - timeout_match = re.search(r"timeout\s*=\s*(\d+)", content) - if timeout_match: - info["timeout"] = int(timeout_match.group(1)) - - # Extract requires_confirmation - if "requires_confirmation=True" in content: - info["requires_confirmation"] = True - - # Extract SkillParams - param_matches = re.findall( - r'SkillParam\(\s*name\s*=\s*"([^"]+)"' - r".*?description\s*=\s*\(\s*(.*?)\s*\)", - content, - re.DOTALL, - ) - for pname, pdesc in param_matches: - pdesc_clean = " ".join(d.strip().strip('"').strip("'") for d in pdesc.split()) - ptype = "string" - type_match = re.search( - rf'SkillParam\(\s*name\s*=\s*"{pname}".*?type\s*=\s*"([^"]+)"', - content, - re.DOTALL, - ) - if type_match: - ptype = type_match.group(1) - info["params"].append( - { - "name": pname, - "type": ptype, - "description": pdesc_clean[:120], - } - ) - - return info - - # ─── Audit Routes ───────────────────────────────────────────────── - - def _audit_routes(self) -> dict: - """Extract all API routes from routes.py.""" - routes = [] - if not ROUTES_FILE.exists(): - return {"success": True, "routes": []} - - content = ROUTES_FILE.read_text() - - # Match: app.router.add_get("/api/...", handle_xyz) - for match in re.finditer( - r'app\.router\.add_(get|post|put|delete)\("([^"]+)"\s*,\s*(\w+)', - content, - ): - routes.append( - { - "method": match.group(1).upper(), - "path": match.group(2), - "handler": match.group(3), - } - ) - - # Also look for try/except block imports - import_blocks = re.findall( - r"from \.(\w+)\s+import\s+(\w+)", - content, - ) - modules_used = list(set(m[0] for m in import_blocks)) - - return { - "success": True, - "routes": sorted(routes, key=lambda r: r["path"]), - "total": len(routes), - "modules_used": sorted(modules_used), - } - - # ─── Audit Secrets ──────────────────────────────────────────────── - - def _audit_secrets(self) -> dict: - """Discover secret keys from env files and configs.""" - secrets = [] - - # Check hivemind .env - if ENV_FILE.exists(): - content = ENV_FILE.read_text() - for match in re.finditer( - r'(#\s*(.*?)\n)?\s*(\w+)\s*=\s*"[^"]*"', - content, - ): - key = match.group(3) - comment = match.group(2) or "" - secrets.append( - { - "key": key, - "scope": "environment", - "store": "~/.config/hivemind/.env", - "description": comment.strip()[:100], - } - ) - - # Check vault.yaml for secret references - if VAULT_CONFIG.exists(): - content = VAULT_CONFIG.read_text() - key_matches = re.findall(r'-\s*"(\w+)"', content) - for key in key_matches: - if any(k.upper() in ("KEY", "TOKEN", "SECRET") for k in [key]): - secrets.append( - { - "key": key, - "scope": "vault", - "store": "~/.local/share/udos/Vault/", - "description": "Referenced in vault.yaml", - } - ) - - return {"success": True, "secrets": secrets, "total": len(secrets)} - - # ─── Audit Variables ────────────────────────────────────────────── - - def _audit_variables(self) -> dict: - """Discover variable scopes and keys.""" - variables = [] - - # From vault.yaml - if VAULT_CONFIG.exists(): - variables.append( - { - "scope": "user", - "file": "~/.local/share/udos/Vault/variables/user.yaml", - "description": "User-level persistent variables", - "examples": ["theme", "editor_font_size", "last_world"], - } - ) - variables.append( - { - "scope": "global", - "file": "~/.local/share/udos/Vault/variables/global.yaml", - "description": "System-wide variables", - "examples": ["runtime_version", "engine"], - } - ) - variables.append( - { - "scope": "snack", - "file": "snack manifest (per-container)", - "description": "Per-snack container state", - "examples": ["level", "player_hp"], - } - ) - variables.append( - { - "scope": "system", - "file": "memory only (not persisted)", - "description": "Runtime-only state", - "examples": ["pid", "uptime_seconds"], - } - ) - - # From variables_api.py - if VARIABLES_API.exists(): - content = VARIABLES_API.read_text() - get_vars = re.findall(r"handle_get_(\w+)_variables", content) - for gv in get_vars: - variables.append( - { - "scope": gv, - "source": "variables_api.py", - "description": f"Exposed via /api/variables/{gv}", - "examples": [], - } - ) - - return {"success": True, "variables": variables, "total": len(variables)} - - # ─── Audit MCP ──────────────────────────────────────────────────── - - def _audit_mcp(self) -> dict: - """Discover MCP servers and bridge components owned by uCore.""" - servers = [] - - bridge_dir = BACKEND_DIR / "mcp" / "mcp_bridge" - if (bridge_dir / "index.ts").exists(): - servers.append( - { - "name": "ucore-bridge", - "type": "stdio", - "file": str((bridge_dir / "index.ts").relative_to(ROOT_DIR)), - "command": "node backend/app/mcp/mcp_bridge/build/index.js", - "cwd": str(ROOT_DIR), - "source": "uCore self-hosted bridge", - } - ) - - # Also check registered MCP tools in the backend - mcp_tools_dir = BACKEND_DIR / "mcp" - if mcp_tools_dir.exists(): - for mcp_mod in mcp_tools_dir.glob("*_server.py"): - servers.append( - { - "name": mcp_mod.stem.replace("_server", ""), - "file": str(mcp_mod.relative_to(ROOT_DIR)), - "source": "backend auto-discovery", - "command": f"python3 -m app.mcp.{mcp_mod.stem}", - "cwd": str(BACKEND_DIR), - } - ) - - return {"success": True, "servers": servers, "total": len(servers)} - - # ─── Audit Paths ────────────────────────────────────────────────── - - def _audit_paths(self) -> dict: - """Discover all important file system paths used by the ecosystem.""" - paths = [] - - # Config paths - config_dirs = [ - str(ROOT_DIR / "config"), - str(ROOT_DIR / "seeds"), - str(ROOT_DIR / "docs"), - str(ROOT_DIR / "backend" / "config"), - str(ROOT_DIR / "frontend-vue" / "src" / "styles"), - ] - for d in config_dirs: - path = Path(d) - if path.exists(): - paths.append( - { - "path": str(path.relative_to(ROOT_DIR)), - "type": "config", - "description": "Configuration directory", - } - ) - - # Runtime paths - runtime_paths = [ - str(settings.udos_home), - str(settings.config_dir), - str(settings.logs_dir), - str(settings.vault_root), - str(settings.shared_vault_root), - str(settings.public_vault_root), - ] - for p in runtime_paths: - paths.append( - { - "path": p, - "type": "runtime", - "description": "User runtime path", - } - ) - - # Skills directory - paths.append( - { - "path": "backend/app/skills/builtin/", - "type": "skills", - "description": "Builtin skills directory", - } - ) - - # API directory - paths.append( - { - "path": "backend/app/api/", - "type": "api", - "description": "REST API handlers", - } - ) - - # MCP directory - paths.append( - { - "path": "backend/app/mcp/", - "type": "mcp", - "description": "MCP servers directory", - } - ) - - # Services - paths.append( - { - "path": "backend/app/services/", - "type": "services", - "description": "Backend service modules", - } - ) - - return {"success": True, "paths": paths, "total": len(paths)} - - # ─── Audit Runtimes ─────────────────────────────────────────────── - - def _audit_runtimes(self) -> dict: - """Discover backend runtime service modules.""" - runtimes = {} - - candidate_modules: dict[str, Path] = { - "dev_layer": BACKEND_DIR / "services" / "dev_layer.py", - "feed_server": BACKEND_DIR / "mcp" / "feed" / "feed_server.py", - "feed_consumer": BACKEND_DIR / "services" / "feed_consumer.py", - "hivemind_server": BACKEND_DIR / "mcp" / "hivemind_server.py", - "llm_router": BACKEND_DIR / "mcp" / "llm_router.py", - "model_pricing": BACKEND_DIR / "services" / "model_pricing.py", - "template_manager": BACKEND_DIR / "services" / "template_manager.py", - } - - for name, path in candidate_modules.items(): - if not path.exists(): - continue - content = path.read_text() - - endpoints = [] - for match in re.findall( - r'"([a-z_]+)"\s*:\s*self\._[a-z_]+', - content, - ): - endpoints.append(match) - for match in re.findall(r'name="([a-z_]+)"', content): - endpoints.append(match) - - variables = {} - for match in re.findall( - r"self\.([a-z_]+)\s*=\s*([^#\n]+)", - content, - ): - key, val = match - if len(key) > 2 and not key.startswith("_"): - variables[key] = val.strip().rstrip(",") - - commands = [] - for match in re.findall(r"(?:async )?def ([a-z_]+)\(", content): - if not match.startswith("_"): - commands.append(match) - - runtimes[name] = { - "file": str(path.relative_to(ROOT_DIR)), - "endpoints": sorted(set(endpoints)), - "variables": variables, - "commands": sorted(set(commands)), - } - - return {"success": True, "runtimes": runtimes, "total": len(runtimes)} - - # ─── Assess (Health Scored) ──────────────────────────────────────── - - def _assess(self, output_path: str) -> dict: - """Full audit with health scoring for every item. - - Runs all sub-audits, then scores each item as: - working | untested | broken | orphaned - - - Skills: checks if file has BaseSkill subclass, no SyntaxErrors - - MCP: checks if server binary/config exists - - Routes: all considered 'working' if parseable - - Runtimes: checks if referenced Python file exists - """ - full = self._report() - eco = full.get("ecosystem", {}) - - assessed: dict[str, list[dict]] = {} - # Score skills - scored_skills = [] - for s in eco.get("skills", {}).get("items", []): - status = "untested" - s_issues: list[str] = [] - file_path = s.get("file", "") - if file_path: - full_path = ROOT_DIR / file_path - if not full_path.exists(): - status = "orphaned" - s_issues.append("File not found at resolved path") - else: - try: - content = full_path.read_text() - has_base = "BaseSkill" in content or "class " in content - has_run = "def run" in content or "async def run" in content - if has_base and has_run: - status = "working" - elif has_run: - status = "working" # module-level run() - else: - status = "untested" - except Exception: - status = "broken" - s_issues.append("Cannot read file") - scored_skills.append( - { - **s, - "health": status, - "issues": s_issues, - } - ) - assessed["skills"] = scored_skills - - # Score MCP servers - scored_mcp = [] - for m in eco.get("mcp_servers", {}).get("items", []): - status = "working" - m_issues: list[str] = [] - cmd = m.get("command", "") - if cmd and not any(Path(c.split()[0]).exists() for c in [cmd]): - # If it's a python module, check the file - if "python" in cmd or "app.mcp" in cmd: - mod_part = cmd.replace("python3 -m ", "").replace("python -m ", "") - mod_path = BACKEND_DIR / "mcp" / (mod_part.split(".")[-1] + ".py") - if not mod_path.exists(): - status = "broken" - m_issues.append(f"Module not found: {mod_path}") - if m.get("disabled"): - status = "untested" - m_issues.append("Server is disabled") - scored_mcp.append( - { - **m, - "health": status, - "issues": m_issues, - } - ) - assessed["mcp_servers"] = scored_mcp - - # Score runtimes - scored_runtimes = [] - for name, rt in eco.get("runtimes", {}).get("items", {}).items(): - status = "working" - r_issues: list[str] = [] - rt_path = ROOT_DIR / rt.get("file", "") - if not rt_path.exists(): - status = "broken" - r_issues.append(f"File not found: {rt.get('file')}") - scored_runtimes.append( - { - "name": name, - **rt, - "health": status, - "issues": r_issues, - } - ) - assessed["runtimes"] = scored_runtimes - - # Routes and paths are always 'working' if discovered - assessed["routes"] = [ - {**r, "health": "working", "issues": []} for r in eco.get("routes", {}).get("items", []) - ] - assessed["paths"] = [ - {**p, "health": "working", "issues": []} for p in eco.get("paths", {}).get("items", []) - ] - assessed["secrets"] = [ - {**s, "health": "working", "issues": []} - for s in eco.get("secrets", {}).get("items", []) - ] - assessed["variables"] = [ - {**v, "health": "working", "issues": []} - for v in eco.get("variables", {}).get("items", []) - ] - - # Aggregate health - all_items = ( - scored_skills - + scored_mcp - + scored_runtimes - + assessed["routes"] - + assessed["paths"] - + assessed["secrets"] - + assessed["variables"] - ) - health_counts = {"working": 0, "untested": 0, "broken": 0, "orphaned": 0} - for item in all_items: - h = item.get("health", "untested") - health_counts[h] = health_counts.get(h, 0) + 1 - - total = sum(health_counts.values()) - health_pct = round((health_counts["working"] / total) * 100, 1) if total > 0 else 0 - - result = { - "success": True, - "action": "assess", - "ecosystem": assessed, - "health": { - "total_items": total, - **health_counts, - "health_pct": health_pct, - }, - "recommendations": self._health_recommendations(health_counts, scored_skills), - } - - # Persist - if output_path: - out = Path(output_path) - out.parent.mkdir(parents=True, exist_ok=True) - out.write_text(json.dumps(result, indent=2, default=str)) - - return result - - @staticmethod - def _health_recommendations(counts: dict, skills: list) -> list[str]: - recs = [] - if counts.get("broken", 0) > 0: - recs.append(f"Fix {counts['broken']} broken items — check error logs for details") - if counts.get("untested", 0) > 0: - recs.append(f"Smoke-test {counts['untested']} untested items to validate they work") - if counts.get("orphaned", 0) > 0: - recs.append( - f"Review {counts['orphaned']} orphaned items — consider archiving or re-wiring" - ) - untested_skills = [s["name"] for s in skills if s.get("health") == "untested"] - if untested_skills: - recs.append( - f"Untested skills: {', '.join(untested_skills[:5])}" - + (f" +{len(untested_skills) - 5}" if len(untested_skills) > 5 else "") - ) - return recs - - # ─── Report / Generate ──────────────────────────────────────────── - - def _report(self) -> dict: - """Full ecosystem report.""" - skills = self._audit_skills() - routes = self._audit_routes() - secrets = self._audit_secrets() - variables = self._audit_variables() - mcp = self._audit_mcp() - paths = self._audit_paths() - runtimes = self._audit_runtimes() - - return { - "success": True, - "action": "report", - "ecosystem": { - "skills": { - "total": skills.get("total", 0), - "items": skills.get("skills", []), - }, - "routes": { - "total": routes.get("total", 0), - "items": routes.get("routes", []), - }, - "secrets": { - "total": secrets.get("total", 0), - "items": secrets.get("secrets", []), - }, - "variables": { - "total": variables.get("total", 0), - "items": variables.get("variables", []), - }, - "mcp_servers": { - "total": mcp.get("total", 0), - "items": mcp.get("servers", []), - }, - "paths": { - "total": paths.get("total", 0), - "items": paths.get("paths", []), - }, - "runtimes": { - "total": runtimes.get("total", 0), - "items": runtimes.get("runtimes", {}), - }, - }, - "summary": { - "total_skills": skills.get("total", 0), - "total_routes": routes.get("total", 0), - "total_secrets": secrets.get("total", 0), - "total_variables": variables.get("total", 0), - "total_mcp_servers": mcp.get("total", 0), - "total_paths": paths.get("total", 0), - "total_runtimes": runtimes.get("total", 0), - }, - } - - def _generate(self, output_path: str) -> dict: - """Generate ecosystem-registry.json to disk.""" - report = self._report() - output = Path(output_path) - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text( - json.dumps(report, indent=2, default=str), - ) - return { - "success": True, - "action": "generate", - "output": str(output), - "size_bytes": output.stat().st_size, - "summary": report.get("summary", {}), - } diff --git a/backend/app/skills/builtin/skill_ucore_index.py b/backend/app/skills/builtin/skill_ucore_index.py deleted file mode 100644 index ee00b460..00000000 --- a/backend/app/skills/builtin/skill_ucore_index.py +++ /dev/null @@ -1,502 +0,0 @@ -#!/usr/bin/env python3 -"""uCore Central Index Skill — Unified index and search across uCore ecosystem - -Provides a comprehensive index of: - - All skills (builtin and custom) - - All MCP servers and tools - - All surfaces and components - - All configuration files and schemas - - All documentation and specs - -Usage: - POST /api/skills/ucore_index/run - Body: { - "search": "filepicker", - "category": "component", - "limit": 10 - } -""" -from __future__ import annotations - -import json -import logging -from pathlib import Path -from typing import Any, Dict, List, Optional - -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.ucore_index") - -# uCore root directory -UCORE_ROOT = Path(__file__).parent.parent.parent.parent.parent -FRONTEND_SRC = UCORE_ROOT / "frontend" / "src" -BACKEND_APP = UCORE_ROOT / "backend" / "app" -DOCS_DIR = UCORE_ROOT / "docs" - - -class UCoreIndexSkill(BaseSkill): - """Central index for uCore ecosystem components and resources.""" - - meta = SkillMeta( - id="ucore_index", - name="uCore Index", - description="Central index and search across uCore ecosystem - skills, surfaces, components, and documentation", - category="system", - timeout=30, - params=[ - SkillParam(name="search", type="string", required=False, description="Search query"), - SkillParam(name="category", type="string", required=False, description="Filter by category (Skill, Surface, Component, Documentation)"), - SkillParam(name="limit", type="integer", required=False, default=20, description="Maximum results to return"), - SkillParam(name="action", type="string", required=False, default="index", description="'index', 'search', or 'health-report'"), - ], - requires_confirmation=False, - ) - - def __init__(self): - super().__init__() - self.index: Dict[str, Any] = { - "meta": { - "version": "1.0.0", - "generated_at": None, - "ucore_version": None, - }, - "skills": [], - "mcp_servers": [], - "surfaces": [], - "components": [], - "config_files": [], - "documentation": [], - } - - async def run(self, **kwargs) -> dict: - """Execute the uCore index skill.""" - action = kwargs.get("action", "index") - search_query = kwargs.get("search", "") - category = kwargs.get("category") - limit = kwargs.get("limit", 20) - - if action == "health-report": - return await self._health_report() - - skill = UCoreIndexSkill() - - if search_query: - result = skill.search(search_query, category, limit) - return { - "success": True, - "action": "search", - "query": search_query, - "category": category, - "results": result, - } - else: - index = skill.generate() - return { - "success": True, - "action": "index", - "index": index, - } - - async def _health_report(self) -> dict: - """Generate health report by cross-referencing audit + runtime checks.""" - import json as _json - import urllib.request - - services: dict[str, dict] = {} - checks = [ - ("ollama", "http://localhost:11434/api/tags"), - ("hivemind", "http://localhost:8490/health"), - ("snackbar", "http://localhost:8484/health"), - ("feed", "http://localhost:8484/api/feed/query?limit=1"), - ] - for name, url in checks: - try: - req = urllib.request.Request(url, method="GET") - with urllib.request.urlopen(req, timeout=2) as resp: - services[name] = { - "online": resp.status < 400, - "status": "running", - } - except Exception: - services[name] = {"online": False, "status": "offline"} - - # Load skill audit report if available - skill_health: dict = {"working": 0, "untested": 0, "broken": 0, "total": 0} - audit_file = UCORE_ROOT / "seeds" / "skill-audit-report.json" - if audit_file.exists(): - try: - audit_data = _json.loads(audit_file.read_text()) - skill_health = { - "working": audit_data.get("working", 0), - "untested": audit_data.get("untested", 0), - "broken": audit_data.get("broken", 0), - "total": audit_data.get("total_skills", 0), - "health_pct": audit_data.get("health_pct", 0), - } - except Exception: - pass - - # Load ecosystem assess report - eco_health: dict = {} - eco_file = UCORE_ROOT / "seeds" / "ecosystem-registry.json" - if eco_file.exists(): - try: - eco_data = _json.loads(eco_file.read_text()) - h = eco_data.get("health", {}) - eco_health = { - "total_items": h.get("total_items", 0), - "working": h.get("working", 0), - "untested": h.get("untested", 0), - "broken": h.get("broken", 0), - "health_pct": h.get("health_pct", 0), - } - except Exception: - pass - - all_online = all(v.get("online") for v in services.values()) - overall = "healthy" if all_online else "degraded" - if sum(1 for v in services.values() if not v.get("online")) > 1: - overall = "critical" - - return { - "success": True, - "action": "health-report", - "overall": overall, - "services": services, - "skill_health": skill_health, - "ecosystem_health": eco_health, - "recommendations": self._health_recs( - services, skill_health, eco_health - ), - } - - @staticmethod - def _health_recs( - services: dict, - skills: dict, - eco: dict, - ) -> list[str]: - recs = [] - offline = [k for k, v in services.items() if not v.get("online")] - if offline: - recs.append( - f"Services offline: {', '.join(offline)} — " - "check startup scripts" - ) - if skills.get("broken", 0) > 0: - recs.append( - f"{skills['broken']} skill(s) broken — " - "run skill-audit for details" - ) - if eco.get("broken", 0) > 0: - recs.append( - f"{eco['broken']} ecosystem items broken — " - "run ecosystem-audit assess" - ) - if not recs: - recs.append("All services healthy, no issues detected") - return recs - - def generate(self) -> Dict[str, Any]: - """Generate comprehensive uCore index.""" - log.info("Generating uCore central index...") - - self.index["meta"]["generated_at"] = str(Path.cwd()) - self.index["meta"]["ucore_version"] = self._get_ucore_version() - - # Index all components - self._index_components() - # Index all skills - self._index_skills() - # Index all MCP servers - self._index_mcp_servers() - # Index all surfaces - self._index_surfaces() - # Index config files - self._index_config_files() - # Index documentation - self._index_documentation() - - log.info(f"Index generated: {len(self.index['skills'])} skills, " - f"{len(self.index['surfaces'])} surfaces, " - f"{len(self.index['components'])} components") - - return self.index - - def search(self, query: str, category: Optional[str] = None, limit: int = 20) -> Dict[str, Any]: - """Search the uCore index.""" - if not query: - return {"results": [], "total": 0} - - query = query.lower() - results: List[Dict[str, Any]] = [] - - # Flatten all indexed items - all_items = [] - for category_name, items in self.index.items(): - if category_name == "meta": - continue - if category and category_name != category: - continue - all_items.extend(items) - - # Search across all fields - for item in all_items: - score = 0 - searchable_text = " ".join(str(v).lower() for v in item.values() if v) - - # Direct match - if query in searchable_text: - score += 10 - - # Partial match - if query in searchable_text or any(query in str(v).lower() for v in item.values()): - score += 5 - - # Field-specific scoring - if "name" in item and query in item["name"].lower(): - score += 3 - if "description" in item and query in item["description"].lower(): - score += 2 - if "path" in item and query in item["path"].lower(): - score += 1 - - if score > 0: - results.append({**item, "_score": score}) - - # Sort by score and limit - results.sort(key=lambda x: x.get("_score", 0), reverse=True) - results = results[:limit] - - return { - "query": query, - "results": results, - "total": len(results), - } - - def _get_ucore_version(self) -> str: - """Get uCore version from package.json.""" - try: - package_json = FRONTEND_SRC.parent / "package.json" - if package_json.exists(): - data = json.loads(package_json.read_text()) - return data.get("version", "unknown") - except Exception as e: - log.warning(f"Failed to read version: {e}") - return "unknown" - - def _index_components(self): - """Index all React components.""" - components_dir = FRONTEND_SRC / "components" - if not components_dir.exists(): - return - - for component_file in components_dir.rglob("*.tsx"): - if component_file.name.startswith("_"): - continue - - try: - content = component_file.read_text() - name = component_file.stem - - # Extract component name from file - import re - match = re.search(r"export\s+(const|function)\s+(\w+)", content) - if not match: - continue - component_name = match.group(2) - - self.index["components"].append({ - "name": component_name, - "type": "React Component", - "path": str(component_file.relative_to(FRONTEND_SRC)), - "description": self._extract_description(content), - "exports": self._extract_exports(content), - }) - except Exception as e: - log.debug(f"Failed to index component {component_file}: {e}") - - def _index_skills(self): - """Index all skills.""" - skills_dir = BACKEND_APP / "skills" / "builtin" - if not skills_dir.exists(): - return - - for skill_file in skills_dir.rglob("*.py"): - if skill_file.name.startswith("_"): - continue - - try: - content = skill_file.read_text() - name = skill_file.stem - - # Extract skill metadata - import re - match = re.search(r'class\s+(\w+)\(BaseSkill\):', content) - if not match: - continue - skill_name = match.group(1) - - # Extract description from docstring - desc_match = re.search(r'"""(.+?)"""', content, re.DOTALL) - description = desc_match.group(1).strip() if desc_match else "No description" - - self.index["skills"].append({ - "name": skill_name, - "type": "Skill", - "path": str(skill_file.relative_to(BACKEND_APP)), - "description": description[:200], # Truncate - }) - except Exception as e: - log.debug(f"Failed to index skill {skill_file}: {e}") - - def _index_mcp_servers(self): - """Index MCP servers and tools.""" - mcp_dir = BACKEND_APP / "mcp" - if not mcp_dir.exists(): - return - - # Check for MCP server files - for mcp_file in mcp_dir.rglob("*.py"): - if mcp_file.name.startswith("_"): - continue - - try: - content = mcp_file.read_text() - name = mcp_file.stem - - # Extract MCP server info - import re - match = re.search(r'class\s+(\w+)\(.*MCP.*\):', content, re.IGNORECASE) - if not match: - continue - mcp_name = match.group(1) - - self.index["mcp_servers"].append({ - "name": mcp_name, - "type": "MCP Server", - "path": str(mcp_file.relative_to(BACKEND_APP)), - "description": "MCP server for tool integration", - }) - except Exception as e: - log.debug(f"Failed to index MCP server {mcp_file}: {e}") - - def _index_surfaces(self): - """Index all UI surfaces.""" - surfaces_dir = FRONTEND_SRC / "surfaces" - if not surfaces_dir.exists(): - return - - for surface_file in surfaces_dir.rglob("*.tsx"): - if surface_file.name.startswith("_"): - continue - - try: - content = surface_file.read_text() - name = surface_file.stem - - # Extract surface info - import re - match = re.search(r'export\s+(const|function)\s+(\w+)', content) - if not match: - continue - surface_name = match.group(2) - - # Extract route if present - route_match = re.search(r'path=["\']([^"\']+)["\']', content) - route = route_match.group(1) if route_match else None - - self.index["surfaces"].append({ - "name": surface_name, - "type": "Surface", - "path": str(surface_file.relative_to(FRONTEND_SRC)), - "route": route, - "description": self._extract_description(content), - }) - except Exception as e: - log.debug(f"Failed to index surface {surface_file}: {e}") - - def _index_config_files(self): - """Index configuration files.""" - config_files = [ - "package.json", - "pnpm-workspace.yaml", - "pyproject.toml", - "vite.config.ts", - "tsconfig.json", - ] - - for config_file in config_files: - config_path = FRONTEND_SRC.parent / config_file - if config_path.exists(): - try: - content = config_path.read_text() - self.index["config_files"].append({ - "name": config_file, - "type": "Configuration", - "path": str(config_path.relative_to(UCORE_ROOT)), - "size": len(content), - }) - except Exception as e: - log.debug(f"Failed to index config {config_file}: {e}") - - def _index_documentation(self): - """Index documentation files.""" - if not DOCS_DIR.exists(): - return - - for doc_file in DOCS_DIR.rglob("*.md"): - try: - content = doc_file.read_text() - name = doc_file.stem - - # Extract title from first heading - import re - title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE) - title = title_match.group(1).strip() if title_match else name - - self.index["documentation"].append({ - "name": title, - "type": "Documentation", - "path": str(doc_file.relative_to(DOCS_DIR)), - "description": content[:150].strip(), - }) - except Exception as e: - log.debug(f"Failed to index doc {doc_file}: {e}") - - def _extract_description(self, content: str) -> str: - """Extract component/surface description from file.""" - import re - - # Look for docstring - doc_match = re.search(r'"""(.+?)"""', content, re.DOTALL) - if doc_match: - desc = doc_match.group(1).strip() - # Clean up - desc = re.sub(r'\s+', ' ', desc) - return desc[:200] - - # Look for comment block - comment_match = re.search(r'/\*\*(.*?)\*/', content, re.DOTALL) - if comment_match: - desc = comment_match.group(1).strip() - desc = re.sub(r'\s+', ' ', desc) - return desc[:200] - - return "No description available" - - def _extract_exports(self, content: str) -> List[str]: - """Extract exported names from component.""" - import re - - exports = [] - # Find all export statements - export_matches = re.findall(r'export\s+(?:const|function|class|interface|type)\s+(\w+)', content) - exports.extend(export_matches) - - # Find default exports - default_match = re.search(r'export\s+default\s+(\w+)', content) - if default_match: - exports.append(f"default: {default_match.group(1)}") - - return exports diff --git a/backend/app/skills/catalogue.json b/backend/app/skills/catalogue.json index db7faa7d..3f56709c 100644 --- a/backend/app/skills/catalogue.json +++ b/backend/app/skills/catalogue.json @@ -10,12 +10,9 @@ {"module": "episodic_log.py", "owner": "uCore", "lifecycle": "review", "risk": "write", "lane": "memory", "allowed_roots": ["UDOS_HOME"]}, {"module": "git_maintenance.py", "owner": "uCore", "lifecycle": "active", "risk": "write", "lane": "developer", "allowed_roots": ["Code"]}, {"module": "lint_fix.py", "owner": "uCore", "lifecycle": "active", "risk": "write", "lane": "developer", "allowed_roots": ["Code"]}, - {"module": "skill_audit.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code"]}, - {"module": "skill_ecosystem_audit.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_mcp_self_heal.py", "owner": "uCore", "lifecycle": "merge", "risk": "write", "lane": "recovery", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_nuggets_and_spool.py", "owner": "uCore", "lifecycle": "split", "risk": "destructive", "lane": "recovery", "allowed_roots": ["UDOS_HOME"]}, {"module": "skill_self_heal.py", "owner": "uCore", "lifecycle": "merge", "risk": "destructive", "lane": "recovery", "allowed_roots": ["Code", "UDOS_HOME"]}, - {"module": "skill_ucore_index.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_vault_discovery.py", "owner": "uKnowledge", "lifecycle": "move", "risk": "read", "lane": "knowledge", "allowed_roots": ["Vault", "Shared", "Public"]}, {"module": "tasker_sync.py", "owner": "uFlow", "lifecycle": "move", "risk": "write", "lane": "workflow", "allowed_roots": ["UDOS_HOME"]}, {"module": "vault_sync.py", "owner": "uKnowledge", "lifecycle": "move", "risk": "write", "lane": "knowledge", "allowed_roots": ["Vault", "Shared", "Public", "UDOS_HOME"]}, diff --git a/backend/health/autonomy_engine.py b/backend/health/autonomy_engine.py deleted file mode 100644 index e68cb775..00000000 --- a/backend/health/autonomy_engine.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Autonomy Engine — scheduled background tasks for uCore ecosystem. - -Runs every 24 hours (or on demand) to: -- Execute ecosystem-audit -- Check health thresholds -- Log results to ``$UDOS_HOME/logs`` -- Alert if health drops below 95% - -Usage as cron job: - */30 * * * * cd ~/Code/uCore && backend/.venv/bin/python -m health.autonomy_engine - -Usage as one-shot: - python -m health.autonomy_engine --once -""" - -from __future__ import annotations - -import json -import logging -import os -import sys -import time -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -UDOS_HOME = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")) -LOG_DIR = UDOS_HOME / "logs" -LOG_DIR.mkdir(parents=True, exist_ok=True) - -AUDIT_LOG = LOG_DIR / "autonomy.log" -STATE_FILE = LOG_DIR / "autonomy_state.json" -HEALTH_THRESHOLD = 95.0 - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [autonomy] %(levelname)s %(message)s", - handlers=[ - logging.FileHandler(AUDIT_LOG), - logging.StreamHandler(sys.stdout), - ], -) -log = logging.getLogger("autonomy") - - -def _call_api( - path: str, method: str = "GET", body: dict | None = None, timeout: int = 120 -) -> dict | None: - """Call the uCore backend API.""" - import urllib.request - - url = f"http://127.0.0.1:8484{path}" - try: - data = None - if body is not None: - data = json.dumps(body).encode("utf-8") - req = urllib.request.Request( - url, - data=data, - headers={"Content-Type": "application/json"} if data else {}, - method=method, - ) - with urllib.request.urlopen(req, timeout=timeout) as resp: - return json.loads(resp.read().decode("utf-8")) - except Exception as e: - log.warning(f"API call failed: {path} — {e}") - return None - - -def run_ecosystem_audit() -> dict[str, Any]: - """Run the ecosystem-audit skill and return parsed results.""" - log.info("Starting ecosystem audit...") - start = time.time() - - result = _call_api( - "/api/skills/ecosystem-audit/run", - method="POST", - body={"action": "assess"}, - timeout=120, - ) - - elapsed = time.time() - start - - if result is None: - return { - "success": False, - "error": "API call failed", - "duration_seconds": round(elapsed, 1), - "timestamp": datetime.now(timezone.utc).isoformat(), - } - - health = result.get("health", {}) - return { - "success": result.get("success", False), - "health_pct": health.get("health_pct", 0), - "working": health.get("working", 0), - "untested": health.get("untested", 0), - "broken": health.get("broken", 0), - "orphaned": health.get("orphaned", 0), - "total_items": health.get("total_items", 0), - "duration_seconds": round(elapsed, 1), - "timestamp": datetime.now(timezone.utc).isoformat(), - } - - -def check_ollama() -> dict[str, Any]: - """Check Ollama status.""" - result = _call_api("/api/ollama/status", timeout=10) - if result is None: - return {"online": False, "error": "unreachable"} - return { - "online": result.get("online", False), - "model_count": result.get("model_count", 0), - } - - -def save_state(state: dict[str, Any]) -> None: - """Persist autonomy state for frontend consumption.""" - try: - STATE_FILE.write_text(json.dumps(state, indent=2, default=str)) - except Exception as e: - log.warning(f"Failed to save state: {e}") - - -def load_state() -> dict[str, Any]: - """Load last known autonomy state.""" - try: - if STATE_FILE.exists(): - return json.loads(STATE_FILE.read_text()) - except Exception: - pass - return {} - - -def run_full_check() -> dict[str, Any]: - """Run all health checks and return combined state.""" - log.info("=== Autonomy Engine: Full Health Check ===") - - audit = run_ecosystem_audit() - ollama = check_ollama() - - health_pct = audit.get("health_pct", 0) - - if audit.get("success"): - if health_pct >= HEALTH_THRESHOLD: - log.info(f"Health OK: {health_pct}% (threshold: {HEALTH_THRESHOLD}%)") - else: - log.warning( - f"Health BELOW threshold: {health_pct}% < {HEALTH_THRESHOLD}% " - f"({audit.get('broken', 0)} broken, {audit.get('untested', 0)} untested)" - ) - else: - log.error(f"Audit failed: {audit.get('error', 'unknown')}") - - if not ollama.get("online"): - log.warning("Ollama is offline") - - state = { - "last_audit": audit, - "ollama": ollama, - "health_pct": health_pct, - "healthy": health_pct >= HEALTH_THRESHOLD, - "threshold": HEALTH_THRESHOLD, - "updated_at": datetime.now(timezone.utc).isoformat(), - } - - save_state(state) - log.info( - f"State saved. Health: {health_pct}% | Ollama: {'online' if ollama.get('online') else 'offline'}" - ) - return state - - -def main() -> None: - """Entry point — run full check and exit.""" - state = run_full_check() - if not state.get("healthy"): - sys.exit(1) - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="uCore Autonomy Engine") - parser.add_argument("--once", action="store_true", help="Run once and exit") - parser.add_argument( - "--interval", type=int, default=86400, help="Seconds between checks (default: 24h)" - ) - args = parser.parse_args() - - if args.once: - state = run_full_check() - print(json.dumps(state, indent=2, default=str)) - else: - log.info(f"Starting autonomy loop (interval: {args.interval}s)") - while True: - try: - run_full_check() - except Exception as e: - log.error(f"Check failed: {e}") - time.sleep(args.interval) diff --git a/backend/tests/test_dev_layer_quick_actions.py b/backend/tests/test_dev_layer_quick_actions.py new file mode 100644 index 00000000..a837696a --- /dev/null +++ b/backend/tests/test_dev_layer_quick_actions.py @@ -0,0 +1,13 @@ +from app.api.dev_layer_api import _QUICK_ACTIONS + + +def test_dev_hud_only_exposes_read_only_working_quick_actions(): + assert _QUICK_ACTIONS == [ + { + "id": "system-health", + "label": "System Health Check", + "icon": "favorite", + "method": "GET", + "path": "/api/health/full", + }, + ] diff --git a/docs/PLATES_SYSTEM_SPEC.md b/docs/PLATES_SYSTEM_SPEC.md index e66a7809..58e23d40 100644 --- a/docs/PLATES_SYSTEM_SPEC.md +++ b/docs/PLATES_SYSTEM_SPEC.md @@ -218,7 +218,7 @@ if report["drift_detected"]: When corruption or hallucination is detected: ``` -1. DETECT → Run integrity checks (mcp_guardrails, skill_audit) +1. DETECT → Run integrity checks (MCP guardrails and capability catalogue) 2. SALVAGE → Extract key state from corrupted instance - Skills: meta.id, meta.params, current state from state.py - Snacks: snack_id, config values @@ -361,10 +361,11 @@ ucore plate promote --from backend/app/skills/builtin/my_skill.py \ ## 11. Drift Detection -The `skill_audit` skill compares running instances against plates: +The capability catalogue and registry discovery test compare enabled modules +against the governed builtin set: ```python -report = await run_skill_by_id("skill_audit", scope="all") +python -m pytest -q backend/tests/test_skill_registry_discovery_policy.py # Returns: # - matched: components that match their plate # - drifted: components that differ (with diff) diff --git a/docs/SKILLS_AUDIT_2026-08-18.md b/docs/SKILLS_AUDIT_2026-08-18.md index eabacdc4..0a0732b9 100644 --- a/docs/SKILLS_AUDIT_2026-08-18.md +++ b/docs/SKILLS_AUDIT_2026-08-18.md @@ -57,8 +57,10 @@ allowed roots, deterministic dry run where relevant and dedicated tests. - `gh-workflow-bridge`: narrow to GitHub issues, Actions, PR/review and Codex handoff with explicit external-write approval. - `brain_sync`: separate deterministic indexing from model synthesis. -- `skill-audit` and `ecosystem-audit`: replace source-text heuristics with - manifest/schema validation and executable tests. +- The source-text `skill-audit`, generated `ecosystem-audit`, and broad + `ucore-index` Skills are retired. The fail-closed capability catalogue, + registry discovery tests, route audit, and `/api/health/full` are the + deterministic validation and runtime-health authorities. ### Merge or split diff --git a/frontend-vue/src/skills/organisms/DevHudPanel.vue b/frontend-vue/src/skills/organisms/DevHudPanel.vue index b751c016..13a81580 100644 --- a/frontend-vue/src/skills/organisms/DevHudPanel.vue +++ b/frontend-vue/src/skills/organisms/DevHudPanel.vue @@ -163,14 +163,20 @@ async function loadHud() { } } -async function triggerAction(action: { id: string; label: string }) { +async function triggerAction(action: { + id: string; + label: string; + method: "GET" | "POST"; + path: string; +}) { runningAction.value = action.id; try { - await fetch(`${SNACKBAR_BASE}/api/skills/${action.id}/run`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); + const options: RequestInit = { method: action.method }; + if (action.method === "POST") { + options.headers = { "Content-Type": "application/json" }; + options.body = JSON.stringify({}); + } + await fetch(`${SNACKBAR_BASE}${action.path}`, options); } catch { // best-effort } finally {