From f328750c79adfb397b4aaa0031dffa0e0f36b174 Mon Sep 17 00:00:00 2001 From: rosspeili Date: Thu, 22 Oct 2026 13:02:26 +0300 Subject: [PATCH] perf(cli): resolve startup latency, scope distribution checks, and doctor env handling - Lazily initialize Draft7Validator in manifest_schema to avoid 4.8s Lark/RFC 3987 compilation on startup - Optimize iter_skillware_distributions with targeted sys.path globbing to prevent linear site-packages metadata scans - Lazily load requests in cli.py via module __getattr__ to defer urllib3 and certifi import overhead - Cache _package_version_str() to avoid redundant distribution lookups during interactive session - Rename deepfake_guard/guard.py to deepfake_engine.py to prevent sys.modules collisions with deceptive_ui_guard - Distinguish fatal doctor errors (SCHEMA, DEPS, LOAD) from missing runtime secrets (ENVS) to exit 0 on clean code - Update CLI usage docs with missing commits and document doctor ENVS notice behavior --- CHANGELOG.md | 9 ++++ docs/usage/cli.md | 4 +- .../{guard.py => deepfake_engine.py} | 0 skills/security/deepfake_guard/skill.py | 4 +- skillware/cli.py | 46 +++++++++++++------ skillware/core/manifest_schema.py | 24 ++++++++-- skillware/version_policy.py | 37 +++++++++++++-- tests/test_cli.py | 3 +- 8 files changed, 102 insertions(+), 25 deletions(-) rename skills/security/deepfake_guard/{guard.py => deepfake_engine.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c65594..437f032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta ## [Unreleased] +### Added + +- **CLI / Performance:** Lazy `Draft7Validator` schema validator initialization in `skillware.core.manifest_schema` (eliminates 4.8s Lark/RFC 3987 compilation on import), targeted `sys.path` distribution discovery in `skillware.version_policy` (eliminates linear metadata scans across all installed packages on startup), and memoized CLI version resolution. +- **CLI / Doctor:** Distinguish hard code integrity failures (`SCHEMA`, `DEPS`, `LOAD`) from missing runtime environment variables (`ENVS`). Missing API keys now print an informational advisory and return exit code 0 rather than terminating with exit code 1. + +### Fixed + +- **Skill (`security/deepfake_guard`):** Renamed internal helper `guard.py` to `deepfake_engine.py` to resolve `sys.modules["guard"]` namespace collision with `security/deceptive_ui_guard` during sequential dynamic skill loading in `skillware doctor`. + ### Changed - **Skill (`finance/wallet_screening` v2.0.0):** Report schema v2 with `verdict` (`ALLOW` / `REVIEW` / `DENY`), deterministic `risk_level`, `risk_factors[]`, and hop-1 `indirect_exposures` / `risky_counterparties`. Dual-emits `summary.risk_flag` and `summary.total_transactions` for v1 hosts ([#439](https://github.com/ARPAHLS/skillware/issues/439) slice 2). diff --git a/docs/usage/cli.md b/docs/usage/cli.md index 596509c..0493041 100644 --- a/docs/usage/cli.md +++ b/docs/usage/cli.md @@ -352,7 +352,7 @@ Check whether skills are valid and can load in the current environment — Manif At the end of the diagnostic table, `skillware doctor` prints a summary of compliant manifests (e.g. `27/27 skill manifests satisfy Manifest v2 specification`). -Exit code is non-zero when any skill fails **SCHEMA**, **DEPS**, **LOAD**, or **ENVS**. For full bundle behavior, use `skillware test`. +Exit code is non-zero (`1`) when any skill fails hard integrity checks (**SCHEMA**, **DEPS**, or **LOAD**). Missing **ENVS** produces an informational notice advising which keys are required before executing those skills, returning exit code `0` so offline health audits succeed without requiring full sets of third-party API keys. For full bundle behavior, use `skillware test`. **`doctor --install`** reports duplicate/orphan `skillware` distributions, suggests Unix and Windows recovery commands, and exits `0` when install metadata is healthy. Also summarized in **`skillware config show`** under **install (this Python)**. @@ -656,6 +656,8 @@ Commits that touched the `skillware` CLI tool, its subcommands, or its documenta | Commit | Description | Date | Version | Contributors | | :--- | :--- | :--- | :--- | :--- | +| [`f522b17`](https://github.com/ARPAHLS/skillware/commit/f522b17c0d) | chore(release): cut v0.5.9 with Manifest v2 Universal Taxonomy, telemetry_normalizer, AcroForm engine, and SPDX licensing | 20 Oct 2026 | `0.5.9` | [@rosspeili](https://github.com/rosspeili) | +| [`831856d`](https://github.com/ARPAHLS/skillware/commit/831856d8c6) | fix(catalog,cli): set optimization skills shape to middleware, restore CHANGELOG Added header, and expand cli usage examples (#440) | 19 Oct 2026 | `0.5.9` | [@rosspeili](https://github.com/rosspeili) | | [`6d175e6`](https://github.com/ARPAHLS/skillware/commit/6d175e6a80) | feat(cli,docs): implement Manifest v2 discovery flags, info command, doctor schema checks, and catalog exporter (#440) | 19 Oct 2026 | `0.5.9` | [@rosspeili](https://github.com/rosspeili) | | [`cdd258f`](https://github.com/ARPAHLS/skillware/commit/cdd258ff63) | docs(cli): sync interactive menu options and introduce CLI history table (#433) | 14 Oct 2026 | `0.5.9` | [@rosspeili](https://github.com/rosspeili) | | [`2f20e20`](https://github.com/ARPAHLS/skillware/commit/2f20e20b08) | feat(core): standardize SPDX license identifiers and copyright headers (#416) (#422) | 11 Oct 2026 | `0.5.9` | [@blade-dancer](https://github.com/bd-c3) | diff --git a/skills/security/deepfake_guard/guard.py b/skills/security/deepfake_guard/deepfake_engine.py similarity index 100% rename from skills/security/deepfake_guard/guard.py rename to skills/security/deepfake_guard/deepfake_engine.py diff --git a/skills/security/deepfake_guard/skill.py b/skills/security/deepfake_guard/skill.py index 03359be..9699c44 100644 --- a/skills/security/deepfake_guard/skill.py +++ b/skills/security/deepfake_guard/skill.py @@ -10,7 +10,7 @@ try: from .document import parse_and_validate_mrz from .forensics import load_image - from .guard import analyze_asset + from .deepfake_engine import analyze_asset from .provenance import extract_metadata_and_provenance except (ImportError, ValueError): import sys @@ -20,7 +20,7 @@ sys.path.insert(0, _pkg_dir) from document import parse_and_validate_mrz from forensics import load_image - from guard import analyze_asset + from deepfake_engine import analyze_asset from provenance import extract_metadata_and_provenance diff --git a/skillware/cli.py b/skillware/cli.py index b46fac9..03d452c 100644 --- a/skillware/cli.py +++ b/skillware/cli.py @@ -11,8 +11,6 @@ from pathlib import Path from typing import List, Dict, Any, Optional, Tuple, Union -import requests - from rich.table import Table from rich.console import Console from rich.text import Text @@ -69,6 +67,15 @@ ) +def __getattr__(name: str) -> Any: + """Lazily load requests to prevent importing urllib3 and certifi during CLI startup.""" + if name == "requests": + import requests + + return requests + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + def _active_theme() -> ThemePalette: """Return the configured palette; config normalization guarantees fallback.""" return active_theme() @@ -393,6 +400,8 @@ def _examples_readme_path() -> Optional[Path]: def _fetch_examples_readme_from_github() -> Optional[str]: """Download examples/README.md from the canonical repo on GitHub.""" + import requests + try: response = requests.get(_EXAMPLES_README_RAW_URL, timeout=10) response.raise_for_status() @@ -1842,7 +1851,8 @@ def cmd_doctor( table.add_column("ENVS", no_wrap=True, ratio=1) table.add_column("DETAIL", style="dim", ratio=4) - failures = 0 + fatal_failures = 0 + missing_envs_count = 0 rows: List[Tuple[str, Text, Text, Text, Text, str]] = [] spinner_label = ( f"Diagnosing {len(skill_ids)} skill(s)…" @@ -1863,13 +1873,11 @@ def cmd_doctor( console.print(str(exc), style=ERROR_STYLE) return 1 - if ( - schema_status == "fail" - or deps_status != "ok" - or load_status == "fail" - or envs_status == "fail" - ): - failures += 1 + if schema_status == "fail" or deps_status != "ok" or load_status == "fail": + fatal_failures += 1 + + if envs_status == "fail": + missing_envs_count += 1 schema_cell = ( Text(schema_status, style=ID_STYLE) @@ -1897,7 +1905,7 @@ def cmd_doctor( elif envs_status == "ok": envs_cell = Text(envs_status, style=ID_STYLE) else: - envs_cell = Text(envs_status, style=ERROR_STYLE) + envs_cell = Text(envs_status, style=CATEGORY_STYLE) rows.append( (sid, schema_cell, deps_cell, load_cell, envs_cell, detail or "—") @@ -1918,7 +1926,19 @@ def cmd_doctor( "See docs/usage/install_extras.md and docs/usage/api_keys.md", style="dim", ) - return 1 if failures else 0 + if missing_envs_count > 0: + console.print( + ( + f"Notice: {missing_envs_count} skill(s) declare required environment " + "variables not set in this environment." + ), + style=CATEGORY_STYLE, + ) + console.print( + "Configure required keys before executing these skills (see docs/usage/api_keys.md).", + style="dim", + ) + return 1 if fatal_failures else 0 def cmd_doctor_install(console=None) -> int: @@ -2138,7 +2158,7 @@ def cmd_interactive(console=None, parser=None) -> None: rc = cmd_doctor(console=console) if rc: console.print( - f" doctor exited with status {rc}", style=ERROR_DIM_STYLE + f" doctor detected issues (exit code {rc})", style=ERROR_DIM_STYLE ) elif command == "mail": mail_nav = cmd_mail_submenu(console=console) diff --git a/skillware/core/manifest_schema.py b/skillware/core/manifest_schema.py index 4b3f5f2..1b84909 100644 --- a/skillware/core/manifest_schema.py +++ b/skillware/core/manifest_schema.py @@ -16,8 +16,6 @@ import re from typing import Any, Dict, List, Mapping, Optional -from jsonschema import Draft7Validator - # ----------------------------------------------------------------------------- # Universal Taxonomy Primitives # ----------------------------------------------------------------------------- @@ -93,7 +91,24 @@ class SkillwareHardwareSafetyWarning(UserWarning): "required": ["name", "version", "description"], } -_MANIFEST_VALIDATOR = Draft7Validator(MANIFEST_SCHEMA) +_MANIFEST_VALIDATOR: Optional[Any] = None + + +def _get_manifest_validator() -> Any: + """Lazily instantiate Draft7Validator to avoid loading Lark/RFC3987 at module import.""" + global _MANIFEST_VALIDATOR + if _MANIFEST_VALIDATOR is None: + from jsonschema import Draft7Validator + + _MANIFEST_VALIDATOR = Draft7Validator(MANIFEST_SCHEMA) + return _MANIFEST_VALIDATOR + + +def __getattr__(name: str) -> Any: + """Provide backward compatibility for external callers accessing _MANIFEST_VALIDATOR.""" + if name == "_MANIFEST_VALIDATOR": + return _get_manifest_validator() + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") def validate_manifest_schema( @@ -115,7 +130,8 @@ def validate_manifest_schema( prefix = f"Skill '{skill_id}': " if skill_id else "" # 1. Structural validation via jsonschema - for err in sorted(_MANIFEST_VALIDATOR.iter_errors(data), key=lambda e: str(e.path)): + validator = _get_manifest_validator() + for err in sorted(validator.iter_errors(data), key=lambda e: str(e.path)): path = ".".join(str(p) for p in err.absolute_path) or "(root)" errors.append(f"{prefix}Schema error at '{path}': {err.message}") diff --git a/skillware/version_policy.py b/skillware/version_policy.py index c7b2a22..14b095c 100644 --- a/skillware/version_policy.py +++ b/skillware/version_policy.py @@ -66,13 +66,42 @@ def _is_invalid_version_string(raw: Optional[str]) -> bool: def iter_skillware_distributions() -> Iterable[metadata.Distribution]: - for dist in metadata.distributions(): + """ + Yield installed distributions for ``skillware`` without scanning all packages on sys.path. + + Targets ``skillware*.dist-info`` and ``skillware*.egg-info`` directories directly across + ``sys.path``, avoiding reading thousands of unrelated metadata records on startup. + Falls back to ``metadata.distribution()`` if targeted filesystem matching yields nothing. + """ + seen_paths = set() + found_any = False + for sp in sys.path: try: - name = (cast(Any, dist.metadata).get("Name") or "").strip() + p = Path(sp) + if not p.is_dir(): + continue + for d in p.glob(f"{PACKAGE_NAME}*"): + if d.suffix in (".dist-info", ".egg-info"): + norm = str(d).lower() + if norm in seen_paths: + continue + seen_paths.add(norm) + try: + dist = metadata.PathDistribution(d) + name = (cast(Any, dist.metadata).get("Name") or "").strip() + if name.lower() == PACKAGE_NAME: + found_any = True + yield dist + except Exception: + pass except Exception: continue - if name.lower() == PACKAGE_NAME: - yield dist + + if not found_any: + try: + yield metadata.distribution(PACKAGE_NAME) + except Exception: + pass def _distribution_dist_info_dir(dist: metadata.Distribution) -> Optional[Path]: diff --git a/tests/test_cli.py b/tests/test_cli.py index 04c6bd9..99762fe 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1071,11 +1071,12 @@ def test_cmd_doctor_reports_missing_env_vars(tmp_path, monkeypatch): buf = io.StringIO() console = Console(file=buf, force_terminal=False, width=120) - assert cmd_doctor(skill_id="finance/needs_key", console=console) == 1 + assert cmd_doctor(skill_id="finance/needs_key", console=console) == 0 output = buf.getvalue() assert "finance/needs_key" in output assert "ETHERSCAN_API_KEY" in output + assert "Notice" in output def test_cmd_doctor_reports_missing_deps(tmp_path, monkeypatch):