From 0e5776496fb92b995096e004e849fc2ea14777f6 Mon Sep 17 00:00:00 2001 From: yaojingang Date: Sun, 19 Apr 2026 11:47:24 +0800 Subject: [PATCH] fix: tighten skill safety hygiene --- .github/workflows/test.yml | 3 ++- reports/context_budget.json | 4 ++-- requirements-ci.txt | 1 + scripts/ci_test.py | 16 +++++++++++++++- scripts/github_benchmark_scan.py | 19 ++++++++++++++++--- scripts/render_intent_dialogue.py | 4 ++-- scripts/render_skill_overview.py | 2 +- scripts/yao.py | 2 +- 8 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 requirements-ci.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 826f8816..a301369c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,5 +12,6 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.11" - - run: python -m pip install --upgrade pip pyyaml + - run: python -m pip install --upgrade pip + - run: python -m pip install --requirement requirements-ci.txt - run: make ci-test diff --git a/reports/context_budget.json b/reports/context_budget.json index 504b1897..527fce3d 100644 --- a/reports/context_budget.json +++ b/reports/context_budget.json @@ -6,9 +6,9 @@ "context_budget_tier": "production", "context_budget_limit": 1000, "skill_body_tokens": 824, - "other_text_tokens": 276829, + "other_text_tokens": 277008, "estimated_initial_load_tokens": 1000, - "estimated_total_text_tokens": 277653, + "estimated_total_text_tokens": 277832, "relevant_file_count": 167, "unused_resource_dirs": [], "quality_signal_points": 140, diff --git a/requirements-ci.txt b/requirements-ci.txt new file mode 100644 index 00000000..f62ce0c5 --- /dev/null +++ b/requirements-ci.txt @@ -0,0 +1 @@ +PyYAML==6.0.3 diff --git a/scripts/ci_test.py b/scripts/ci_test.py index ad5eadf9..abba48c0 100644 --- a/scripts/ci_test.py +++ b/scripts/ci_test.py @@ -9,6 +9,18 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent +SAFE_ENV_KEYS = ( + "HOME", + "LANG", + "LC_ALL", + "PATH", + "PYTHONPATH", + "PYTHONIOENCODING", + "TEMP", + "TERM", + "TMP", + "TMPDIR", +) DEFAULT_TARGETS = [ "eval", "eval-suite", @@ -57,6 +69,8 @@ def run_target(target: str, index: int, total: int, tail_lines: int) -> None: start = time.perf_counter() with tempfile.NamedTemporaryFile(prefix=f"ci-{target}-", suffix=".log", delete=False) as handle: log_path = Path(handle.name) + child_env = {key: os.environ[key] for key in SAFE_ENV_KEYS if key in os.environ} + child_env["CI"] = "1" try: with log_path.open("w", encoding="utf-8") as log_file: proc = subprocess.run( @@ -65,7 +79,7 @@ def run_target(target: str, index: int, total: int, tail_lines: int) -> None: stdout=log_file, stderr=subprocess.STDOUT, text=True, - env={**os.environ, "CI": "1"}, + env=child_env, ) elapsed = time.perf_counter() - start if proc.returncode != 0: diff --git a/scripts/github_benchmark_scan.py b/scripts/github_benchmark_scan.py index da9dcd4e..6007482e 100644 --- a/scripts/github_benchmark_scan.py +++ b/scripts/github_benchmark_scan.py @@ -5,7 +5,7 @@ import os import re from pathlib import Path from typing import Any -from urllib.parse import quote +from urllib.parse import quote, urlparse from urllib.request import Request, urlopen @@ -15,6 +15,7 @@ USER_AGENT = "yao-meta-skill/1.0" DEFAULT_TOP_N = 3 DEFAULT_PER_PAGE = 8 MIN_STARS = 50 +ALLOWED_API_HOSTS = {"api.github.com"} STOPWORDS = { "the", @@ -100,8 +101,15 @@ def github_headers() -> dict[str, str]: return headers +def ensure_allowed_api_url(url: str) -> str: + parsed = urlparse(url) + if parsed.scheme != "https" or parsed.hostname not in ALLOWED_API_HOSTS: + raise ValueError(f"Refusing outbound request to non-GitHub API host: {url}") + return url + + def api_get(url: str) -> dict[str, Any]: - request = Request(url, headers=github_headers()) + request = Request(ensure_allowed_api_url(url), headers=github_headers()) with urlopen(request, timeout=15) as response: # nosec B310 - GitHub API fetch data = response.read().decode("utf-8") return json.loads(data) @@ -154,7 +162,10 @@ def fetch_readme(full_name: str, fixture_bundle: dict[str, Any] | None = None) - return fixture_bundle.get("readmes", {}).get(full_name, "") url = f"{API_ROOT}/repos/{full_name}/readme" - request = Request(url, headers={**github_headers(), "Accept": "application/vnd.github.raw+json"}) + request = Request( + ensure_allowed_api_url(url), + headers={**github_headers(), "Accept": "application/vnd.github.raw+json"}, + ) with urlopen(request, timeout=15) as response: # nosec B310 - GitHub API fetch return response.read().decode("utf-8", errors="replace") @@ -344,6 +355,7 @@ def run_github_benchmark_scan( "ok": True, "query": query, "source": "fixture" if fixture_bundle is not None else "github-api", + "network_boundary": "github-api-only", "repositories": repo_summaries, "cross_repo": cross_repo, "external_references": [repo_to_external_reference(repo) for repo in repo_summaries], @@ -355,6 +367,7 @@ def run_github_benchmark_scan( "ok": False, "query": query, "source": "fixture" if fixture_bundle is not None else "github-api", + "network_boundary": "github-api-only", "repositories": [], "cross_repo": { "borrow": [], diff --git a/scripts/render_intent_dialogue.py b/scripts/render_intent_dialogue.py index dc165673..7d18e4e5 100644 --- a/scripts/render_intent_dialogue.py +++ b/scripts/render_intent_dialogue.py @@ -52,7 +52,7 @@ def classify_focus(description: str) -> str: def build_questions(focus: str) -> list[dict]: base = [ { - "question": "If this skill worked beautifully, what recurring job would it quietly take off the user's plate every time?", + "question": "If this skill worked beautifully, what recurring job would it reliably handle for the user every time?", "why": "This reveals the real job-to-be-done and gives the package a humane center instead of a guessed prompt shape.", }, { @@ -156,7 +156,7 @@ def build_summary(skill_dir: Path) -> dict: ], "opening_styles": opening_styles, "optional_scaffold": [ - "The repeated job it should take over", + "The repeated job it should reliably handle", "The real inputs people will hand to it", "The useful output it should hand back", "What it should clearly refuse", diff --git a/scripts/render_skill_overview.py b/scripts/render_skill_overview.py index a67849e0..5f569b09 100644 --- a/scripts/render_skill_overview.py +++ b/scripts/render_skill_overview.py @@ -185,7 +185,7 @@ def card_items(interface_data: dict, logic_steps: list[str], package_map: list[d def introduction_lines(description: str) -> list[str]: return [ - f"This skill is trying to quietly take over: {description}", + f"This skill is designed to handle: {description}", "Start by clarifying the recurring job, the real input shape, and the output that lets the next person keep moving.", "If the idea is still fuzzy, use the intent dialogue first and let the structure come second.", ] diff --git a/scripts/yao.py b/scripts/yao.py index f2628d26..406b6d29 100644 --- a/scripts/yao.py +++ b/scripts/yao.py @@ -226,7 +226,7 @@ def command_quickstart(args: argparse.Namespace) -> int: sys.stderr.write("Before we deepen the package, I will also look at a few strong public GitHub references so we can borrow patterns without copying them.\n") name = args.name or prompt_with_default("Skill name", "my-skill") job = args.job or prompt_with_default( - "In your own words, what repeated work do you most want this skill to quietly take over", + "In your own words, what repeated work do you most want this skill to reliably handle", "Turn a repeated workflow into a reusable skill.", ) primary_output = args.primary_output or prompt_with_default(