Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 3 additions & 1 deletion docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**.

Expand Down Expand Up @@ -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) |
Expand Down
4 changes: 2 additions & 2 deletions skills/security/deepfake_guard/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down
46 changes: 33 additions & 13 deletions skillware/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)…"
Expand All @@ -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)
Expand Down Expand Up @@ -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 "—")
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 20 additions & 4 deletions skillware/core/manifest_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import re
from typing import Any, Dict, List, Mapping, Optional

from jsonschema import Draft7Validator

# -----------------------------------------------------------------------------
# Universal Taxonomy Primitives
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -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(
Expand All @@ -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}")

Expand Down
37 changes: 33 additions & 4 deletions skillware/version_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading