mirror of
https://github.com/usestrix/strix.git
synced 2026-08-25 12:22:37 +02:00
Five rounds of sweep across the tree. Net ~544 lines removed. Removed: - Section-divider banners and one-line section labels (# Display utilities, # ----- list_requests -----, # CVSS breakdown, etc.). - Module-level prose docstrings on internal modules. Kept one-line summaries; trimmed multi-paragraph narration about SDK/Strix responsibility splits, cache strategies, three-source precedence. - Internal-helper docstrings that just restate the function name — caido_api helpers (caido_url, get_client, view_request, etc.), settings-class one-liners (LLMSettings, RuntimeSettings, ...), UI helper docstrings. - Args/Returns blocks on non-LLM-facing internal helpers (build_strix_agent, render_system_prompt, create_or_reuse, bootstrap_caido) — kept only the genuinely non-obvious params. - Internal-history phrasing — "Mirrors main-branch shape", "pre-SDK harness", "previous lookup matched no attribute". - Narrative comments inside function bodies that explained what the next line does, design rationale obvious from the surrounding code, or "we used to..." asides. - Trailing periods on every error-string literal across the tool tree. - Duplicated roundtripTime quirk comment (kept the LLM-facing copy in tools/proxy/tools.py). Kept (every one names an upstream bug, vendored-code provenance, or non-obvious data quirk): - core/runner.py: SDK replay-with-empty-initial-input + on_agent_end lifecycle gap. - runtime/docker_client.py: VERBATIM COPY block of the upstream _create_container body, pinned to SDK v0.14.6. - runtime/session_manager.py: NO_PROXY for agent-browser CDP loopback. - tools/proxy/caido_api.py: generated-pydantic Request.raw quirk, replay double-history pitfall. - tools/proxy/tools.py: Caido roundtripTime=0 quirk for proxy captures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
import logging
|
|
import re
|
|
from collections.abc import Iterator
|
|
|
|
from strix.utils.resource_paths import get_strix_resource_path
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL)
|
|
|
|
_INTERNAL_SKILL_CATEGORIES: frozenset[str] = frozenset({"scan_modes", "coordination"})
|
|
|
|
|
|
def _iter_user_skill_files() -> Iterator[tuple[str, str]]:
|
|
"""Yield ``(category_name, skill_name)`` for every user-selectable skill."""
|
|
skills_dir = get_strix_resource_path("skills")
|
|
if not skills_dir.exists():
|
|
return
|
|
for category_dir in sorted(skills_dir.iterdir()):
|
|
if not category_dir.is_dir() or category_dir.name.startswith("__"):
|
|
continue
|
|
if category_dir.name in _INTERNAL_SKILL_CATEGORIES:
|
|
continue
|
|
for file_path in sorted(category_dir.glob("*.md")):
|
|
yield category_dir.name, file_path.stem
|
|
|
|
|
|
def get_all_skill_names() -> set[str]:
|
|
"""Return every user-selectable skill name (bare, no category prefix)."""
|
|
return {name for _, name in _iter_user_skill_files()}
|
|
|
|
|
|
def get_available_skills() -> dict[str, list[str]]:
|
|
grouped: dict[str, list[str]] = {}
|
|
for category, name in _iter_user_skill_files():
|
|
grouped.setdefault(category, []).append(name)
|
|
return grouped
|
|
|
|
|
|
def validate_requested_skills(skill_list: list[str], max_skills: int = 5) -> str | None:
|
|
"""Validate a list of user-passed skill names.
|
|
|
|
Returns ``None`` on success, or a model-readable error message
|
|
describing what was wrong (count exceeded, unknown names).
|
|
"""
|
|
if len(skill_list) > max_skills:
|
|
return (
|
|
f"Cannot specify more than {max_skills} skills per agent; "
|
|
f"got {len(skill_list)}. Aim for 1-3 related skills per specialist."
|
|
)
|
|
if not skill_list:
|
|
return None
|
|
available = get_all_skill_names()
|
|
invalid = sorted({s for s in skill_list if s not in available})
|
|
if invalid:
|
|
return f"Invalid skill name(s): {invalid}. Available skills: {sorted(available)}"
|
|
return None
|
|
|
|
|
|
def load_skills(skill_names: list[str]) -> dict[str, str]:
|
|
"""Load skill markdown bodies (frontmatter stripped) by name.
|
|
|
|
Skill files live at ``strix/skills/<category>/<name>.md``. Names
|
|
can be ``"name"`` (any category), ``"category/name"``, or a bare
|
|
file at the skills root. Missing skills are logged and skipped.
|
|
"""
|
|
skills_dir = get_strix_resource_path("skills")
|
|
if not skills_dir.exists():
|
|
return {}
|
|
|
|
by_category: dict[str, str] = {}
|
|
for category_dir in skills_dir.iterdir():
|
|
if not category_dir.is_dir() or category_dir.name.startswith("__"):
|
|
continue
|
|
for file_path in category_dir.glob("*.md"):
|
|
by_category[file_path.stem] = f"{category_dir.name}/{file_path.stem}.md"
|
|
|
|
skill_content: dict[str, str] = {}
|
|
for skill_name in skill_names:
|
|
rel_path: str | None
|
|
if "/" in skill_name:
|
|
rel_path = f"{skill_name}.md"
|
|
elif skill_name in by_category:
|
|
rel_path = by_category[skill_name]
|
|
elif (skills_dir / f"{skill_name}.md").exists():
|
|
rel_path = f"{skill_name}.md"
|
|
else:
|
|
rel_path = None
|
|
|
|
if rel_path is None or not (skills_dir / rel_path).exists():
|
|
logger.warning("Skill not found: %s", skill_name)
|
|
continue
|
|
|
|
try:
|
|
content = (skills_dir / rel_path).read_text(encoding="utf-8")
|
|
except (OSError, ValueError) as e:
|
|
logger.warning("Failed to load skill %s: %s", skill_name, e)
|
|
continue
|
|
|
|
var_name = skill_name.split("/")[-1]
|
|
skill_content[var_name] = _FRONTMATTER_PATTERN.sub("", content).lstrip()
|
|
logger.debug("Loaded skill: %s -> %s", skill_name, var_name)
|
|
|
|
logger.debug("load_skills: %d skill(s) resolved", len(skill_content))
|
|
return skill_content
|