diff --git a/strix/skills/__init__.py b/strix/skills/__init__.py index 082a4369..bc2d3b40 100644 --- a/strix/skills/__init__.py +++ b/strix/skills/__init__.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import TypeGuard import yaml +from jinja2 import Environment, StrictUndefined from strix.utils.resource_paths import get_strix_resource_path @@ -13,9 +14,14 @@ from strix.utils.resource_paths import get_strix_resource_path logger = logging.getLogger(__name__) _FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(?P.*?)\n---\s*\n", re.DOTALL) -# Skill text between `` and `` is kept only for models -# that accept images; text between `` markers only for those that don't. -_MODALITY_BLOCK_PATTERN = re.compile(r"\n(.*?)\n", re.DOTALL) +# Only skills with `template: jinja` in their frontmatter are rendered; the rest hold +# literal `{{ }}` payloads. +_SKILL_TEMPLATES = Environment( + autoescape=False, # noqa: S701 - prompts, not HTML + undefined=StrictUndefined, + trim_blocks=True, + lstrip_blocks=True, +) _INTERNAL_SKILL_CATEGORIES: frozenset[str] = frozenset({"scan_modes", "coordination", "analysis"}) _ROOT_SKILL_CATEGORY = "root" @@ -276,7 +282,6 @@ def load_skills(skill_names: list[str], *, supports_images: bool = True) -> dict if not search_dirs: return {} - keep = "images" if supports_images else "text-only" skill_content: dict[str, str] = {} for skill_name in skill_names: candidates = _candidate_skill_files(skill_name) @@ -295,10 +300,12 @@ def load_skills(skill_names: list[str], *, supports_images: bool = True) -> dict continue var_name = skill_name.split("/")[-1] - _, skill_body = _parse_skill_content(content, file_path) - skill_content[var_name] = _MODALITY_BLOCK_PATTERN.sub( - lambda m: m[2] if m[1] == keep else "", skill_body - ) + metadata, skill_body = _parse_skill_content(content, file_path) + if metadata.get("template") == "jinja": + skill_body = _SKILL_TEMPLATES.from_string(skill_body).render( + supports_images=supports_images + ) + skill_content[var_name] = skill_body logger.debug("Loaded skill: %s -> %s", skill_name, var_name) _track_skill_loaded(var_name, file_path) diff --git a/strix/skills/tooling/agent_browser.md b/strix/skills/tooling/agent_browser.md index 0e89c2db..90dfd4ed 100644 --- a/strix/skills/tooling/agent_browser.md +++ b/strix/skills/tooling/agent_browser.md @@ -1,6 +1,7 @@ --- name: agent_browser description: agent-browser CLI for headless Chrome via shell. Snapshot-and-ref workflow, click/fill/extract, screenshots, multi-tab, multi-session, network mocking. Pre-installed in the sandbox; invoke via exec_command. +template: jinja --- @@ -261,7 +262,7 @@ only for simple expressions. ### Screenshot `agent-browser screenshot` writes a PNG to disk in the sandbox. - +{% if supports_images %} The shell command alone does **not** put the image into your context — chain it with the SDK ``view_image`` tool to actually see it: @@ -276,13 +277,12 @@ the full path on stdout — pass that to ``view_image``). If you need a specific filename, keep it inside that directory or a sibling hidden dir under ``/workspace``. Never write screenshots to ``/tmp`` — ``view_image`` rejects anything outside the workspace root. - - +{% else %} You are running on a text-only model and cannot view images, so a screenshot is only useful as a saved file (for example, login evidence). Drive the page from `snapshot -i` refs, `eval` for any DOM/JS state you need to read, and `text @ref` / `get text` for content extraction. - +{% endif %} ```bash agent-browser screenshot # path printed on stdout @@ -291,7 +291,7 @@ agent-browser screenshot --full # full scroll height agent-browser screenshot --annotate # numbered labels + legend keyed to snapshot refs ``` - +{% if supports_images %} `--annotate` is designed for multimodal models: each label `[N]` maps to ref `@eN`. Take the annotated screenshot, then ``view_image`` it, and you can correlate visual layout with snapshot refs. @@ -307,7 +307,7 @@ supported", or similar), you are running on a text-only model — stop calling it and stop taking screenshots. Drive the page entirely from `snapshot -i` refs, `eval` for any DOM/JS state you need to read, and `text @ref` / `get text` for content extraction. - +{% endif %} ### Handle multiple pages via tabs diff --git a/tests/test_skill_dir_extension.py b/tests/test_skill_dir_extension.py index caa867cc..1aae8364 100644 --- a/tests/test_skill_dir_extension.py +++ b/tests/test_skill_dir_extension.py @@ -70,16 +70,15 @@ def test_registered_dir_adds_new_skill(tmp_path: Path) -> None: assert load_skills(["widget"]) == {"widget": "widget body"} -def test_skill_modality_blocks_follow_image_support(tmp_path: Path) -> None: - body = ( - "a\n\nlook\n\n" - "\nread\n\nb" - ) - _write_skill(tmp_path, "extra", "widget", body) +def test_only_jinja_skills_are_rendered(tmp_path: Path) -> None: + body = "{% if supports_images %}\nlook\n{% else %}\nread\n{% endif %}\n" + _write_skill(tmp_path, "extra", "plain", body) + _write_skill(tmp_path, "extra", "templated", f"---\ntemplate: jinja\n---\n{body}") register_skill_dir(tmp_path) - assert load_skills(["widget"]) == {"widget": "a\nlook\nb"} - assert load_skills(["widget"], supports_images=False) == {"widget": "a\nread\nb"} + assert load_skills(["plain"]) == {"plain": body} + assert load_skills(["templated"]) == {"templated": "look\n"} + assert load_skills(["templated"], supports_images=False) == {"templated": "read\n"} def test_available_skill_includes_frontmatter_description(tmp_path: Path) -> None: