feat(skills): add opt-in AST deep diagnostics

Add opt-in AST diagnostics for skill review without making Skills Guard stricter by default.

- Add hermes skills inspect --ast-deep to scan fetched skill bundles before installation
- Add hermes skills audit --deep to scan already-installed hub skills
- Keep AST analysis in tools/skills_ast_audit.py, separate from tools/skills_guard.py
- Label output as diagnostic hints, not security verdicts
- Cover dynamic import/access patterns: importlib, __import__(computed), getattr(computed), and __dict__[computed]

This follows the maintainer guidance from closed PR #7436: useful AST-level analysis belongs in an opt-in diagnostic path, not in Skills Guard's default heuristic scan.
This commit is contained in:
Tranquil-Flow 2026-05-23 10:06:51 +00:00 committed by Teknium
parent 86871ee25a
commit 7255050c99
5 changed files with 704 additions and 12 deletions

View file

@ -164,7 +164,7 @@ COMMAND_REGISTRY: list[CommandDef] = [
cli_only=True),
CommandDef("skills", "Search, install, inspect, or manage skills",
"Tools & Skills", cli_only=True,
subcommands=("search", "browse", "inspect", "install")),
subcommands=("search", "browse", "inspect", "install", "audit")),
CommandDef("bundles", "List skill bundles (aliases /<name> for multiple skills)",
"Tools & Skills"),
CommandDef("cron", "Manage scheduled tasks", "Tools & Skills",

View file

@ -12267,6 +12267,11 @@ Examples:
"inspect", help="Preview a skill without installing"
)
skills_inspect.add_argument("identifier", help="Skill identifier")
skills_inspect.add_argument(
"--ast-deep",
action="store_true",
help="Run AST-level diagnostics on Python files before installing",
)
skills_list = skills_subparsers.add_parser("list", help="List installed skills")
skills_list.add_argument(
@ -12301,6 +12306,11 @@ Examples:
skills_audit.add_argument(
"name", nargs="?", help="Specific skill to audit (default: all)"
)
skills_audit.add_argument(
"--deep",
action="store_true",
help="Run AST-level analysis on Python files (opt-in diagnostic)",
)
skills_uninstall = skills_subparsers.add_parser(
"uninstall", help="Remove a hub-installed skill"

View file

@ -630,8 +630,13 @@ def do_install(identifier: str, category: str = "", force: bool = False,
c.print("[dim]Use /reset to start a new session now, or --now to activate immediately (invalidates prompt cache).[/]\n")
def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
"""Preview a skill's SKILL.md content without installing."""
def do_inspect(identifier: str, console: Optional[Console] = None,
ast_deep: bool = False) -> None:
"""Preview a skill's SKILL.md content without installing.
When ``ast_deep=True``, also runs AST-level diagnostics against Python
files in the fetched bundle before installation.
"""
from tools.skills_hub import GitHubAuth, create_source_router
c = console or _console
@ -677,6 +682,11 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
preview += f"\n\n... ({len(lines) - 50} more lines)"
c.print(Panel(preview, title="SKILL.md Preview", subtitle="hermes skills install <id> to install"))
if bundle and ast_deep:
from tools.skills_ast_audit import ast_scan_bundle_files, format_ast_report
ast_findings = ast_scan_bundle_files(bundle.files)
c.print(format_ast_report(ast_findings, skill_name=meta.name))
c.print()
@ -906,8 +916,13 @@ def do_update(name: Optional[str] = None, console: Optional[Console] = None) ->
c.print(f"[bold green]Updated {len(updates)} skill(s).[/]\n")
def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> None:
"""Re-run security scan on installed hub skills."""
def do_audit(name: Optional[str] = None, console: Optional[Console] = None,
deep: bool = False) -> None:
"""Re-run security scan on installed hub skills.
When ``deep=True``, also runs AST-level analysis on Python files
(opt-in diagnostic, not a security gate).
"""
from tools.skills_hub import HubLockFile, SKILLS_DIR
from tools.skills_guard import scan_skill, format_scan_report
@ -928,6 +943,10 @@ def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> N
c.print(f"\n[bold]Auditing {len(targets)} skill(s)...[/]\n")
ast_scan_skill = format_ast_report = None
if deep:
from tools.skills_ast_audit import ast_scan_skill, format_ast_report
for entry in targets:
skill_path = SKILLS_DIR / entry["install_path"]
if not skill_path.exists():
@ -936,6 +955,11 @@ def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> N
result = scan_skill(skill_path, source=entry.get("identifier", entry["source"]))
c.print(format_scan_report(result))
if deep:
ast_findings = ast_scan_skill(skill_path)
c.print(format_ast_report(ast_findings, skill_name=entry["name"]))
c.print()
@ -1332,7 +1356,7 @@ def skills_command(args) -> None:
skip_confirm=getattr(args, "yes", False),
name_override=getattr(args, "name", "") or "")
elif action == "inspect":
do_inspect(args.identifier)
do_inspect(args.identifier, ast_deep=getattr(args, "ast_deep", False))
elif action == "list":
do_list(
source_filter=args.source,
@ -1343,7 +1367,8 @@ def skills_command(args) -> None:
elif action == "update":
do_update(name=getattr(args, "name", None))
elif action == "audit":
do_audit(name=getattr(args, "name", None))
do_audit(name=getattr(args, "name", None),
deep=getattr(args, "deep", False))
elif action == "uninstall":
do_uninstall(args.name)
elif action == "reset":
@ -1389,12 +1414,15 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
/skills install openai/skills/skill-creator --force
/skills install https://example.com/path/SKILL.md
/skills inspect openai/skills/skill-creator
/skills inspect openai/skills/skill-creator --ast-deep
/skills list
/skills list --source hub
/skills check
/skills update
/skills audit
/skills audit my-skill
/skills audit --deep
/skills audit my-skill --deep
/skills uninstall my-skill
/skills tap list
/skills tap add owner/repo
@ -1486,10 +1514,11 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
name_override=name_override, console=c)
elif action == "inspect":
if not args:
c.print("[bold red]Usage:[/] /skills inspect <identifier>\n")
non_flag_args = [arg for arg in args if not arg.startswith("--")]
if not non_flag_args:
c.print("[bold red]Usage:[/] /skills inspect <identifier> [--ast-deep]\n")
return
do_inspect(args[0], console=c)
do_inspect(non_flag_args[0], console=c, ast_deep="--ast-deep" in args)
elif action == "list":
source_filter = "all"
@ -1509,8 +1538,9 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
do_update(name=name, console=c)
elif action == "audit":
name = args[0] if args else None
do_audit(name=name, console=c)
name = args[0] if args and not args[0].startswith("--") else None
deep = "--deep" in args
do_audit(name=name, console=c, deep=deep)
elif action == "uninstall":
if not args: