From 9d94d52a21a48da7c89999f2bcd701c8bcf89dd9 Mon Sep 17 00:00:00 2001 From: RomanOstr <46819322+RomanOstr@users.noreply.github.com> Date: Mon, 3 Aug 2026 03:34:23 +0300 Subject: [PATCH 1/2] Make PixelTops runtime deployment-compatible --- README.md | 15 +- deploy/deploy.yml | 20 + deploy/runtime/inpaint-requirements.txt | 107 ++++ deploy/runtime/mask-requirements.txt | 44 ++ scripts/install-runtime.py | 433 +++++++++++++++ scripts/install-skills-bootstrap.py | 503 ++++++++++++++++++ scripts/install-skills.py | 123 ----- scripts/runtime-validation.py | 283 ++++++++++ scripts/validate_repository.py | 11 +- skills/pixeltops-image-editor/SKILL.md | 13 +- .../references/runtime-contract.json | 33 ++ .../scripts/image_edit.py | 53 +- .../scripts/inpaint_worker.py | 10 - .../scripts/mask_worker.py | 57 +- skills/sections/core.md | 70 +++ {templates => skills}/skill-sections.json | 8 +- templates/sections/core.md | 19 - tests/test_validate_repository.py | 35 +- 18 files changed, 1632 insertions(+), 205 deletions(-) create mode 100644 deploy/deploy.yml create mode 100644 deploy/runtime/inpaint-requirements.txt create mode 100644 deploy/runtime/mask-requirements.txt create mode 100644 scripts/install-runtime.py create mode 100644 scripts/install-skills-bootstrap.py delete mode 100644 scripts/install-skills.py create mode 100644 scripts/runtime-validation.py create mode 100644 skills/pixeltops-image-editor/references/runtime-contract.json create mode 100644 skills/sections/core.md rename {templates => skills}/skill-sections.json (74%) delete mode 100644 templates/sections/core.md diff --git a/README.md b/README.md index 096183a..1db4557 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,15 @@ deterministic runtime helpers. | --- | --- | | `pixeltops-image-editor` | Select, erase, fill, composite, resize, and verify raster images with explicit change boundaries. | -## Install +## First install -Run `python scripts/install-skills.py --repo-root . --skill -pixeltops-image-editor` after the repository bootstrap is synchronized. The -managed installer validates the selected skill before installation. +Run `python scripts/install-skills-bootstrap.py` only for the first skill +installation. + +## Deploy + +Repository deployment runs `install-runtime`, then `runtime-validation`, before +handing managed skill installation to `ceratops-skill-lifecycle/deploy`. The +runtime installer requires `uv` on `PATH` and stores machine-local environments +and models under `$CODEX_HOME/tools/masked-image-edit`. Regular skill usage +never installs or validates the runtime. diff --git a/deploy/deploy.yml b/deploy/deploy.yml new file mode 100644 index 0000000..6904922 --- /dev/null +++ b/deploy/deploy.yml @@ -0,0 +1,20 @@ +version: 1 +kind: ceratops-deploy +operations: + deploy: + steps: + - id: install-runtime + run: + - python + - scripts/install-runtime.py + - id: runtime-validation + run: + - python + - scripts/runtime-validation.py + handoff: ceratops-skill-lifecycle/deploy + bootstrap: + steps: + - id: bootstrap-skills + run: + - python + - scripts/install-skills-bootstrap.py diff --git a/deploy/runtime/inpaint-requirements.txt b/deploy/runtime/inpaint-requirements.txt new file mode 100644 index 0000000..f554623 --- /dev/null +++ b/deploy/runtime/inpaint-requirements.txt @@ -0,0 +1,107 @@ +accelerate==1.14.0 +aiofiles==23.2.1 +altair==5.5.0 +annotated-doc==0.0.4 +annotated-types==0.7.0 +antlr4-python3-runtime==4.9.3 +anyio==4.14.2 +attrs==26.1.0 +bidict==0.23.1 +certifi==2026.7.22 +charset-normalizer==3.4.9 +click==8.4.2 +colorama==0.4.6 +contourpy==1.3.2 +controlnet-aux==0.0.3 +cycler==0.12.1 +diffusers==0.27.2 +easydict==1.13 +einops==0.8.2 +exceptiongroup==1.3.1 +fastapi==0.108.0 +ffmpy==1.0.0 +filelock==3.32.0 +fonttools==4.63.0 +fsspec==2026.6.0 +gradio==4.21.0 +gradio-client==0.12.0 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +huggingface-hub==0.25.2 +idna==3.18 +imageio==2.37.4 +importlib-metadata==9.0.0 +importlib-resources==6.5.2 +iopaint==1.6.0 +jinja2==3.1.6 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +kiwisolver==1.5.0 +lazy-loader==0.5 +loguru==0.7.3 +markdown-it-py==4.2.0 +markupsafe==2.1.5 +matplotlib==3.10.9 +mdurl==0.1.2 +mpmath==1.3.0 +narwhals==2.24.0 +networkx==3.4.2 +numpy==1.26.4 +omegaconf==2.3.1 +opencv-python==4.11.0.86 +orjson==3.11.9 +packaging==26.2 +pandas==2.3.3 +peft==0.7.1 +piexif==1.1.3 +pillow==9.5.0 +psutil==7.2.2 +pydantic==2.13.4 +pydantic-core==2.46.4 +pydub==0.25.1 +pygments==2.20.0 +pyparsing==3.3.2 +python-dateutil==2.9.0.post0 +python-engineio==4.13.3 +python-multipart==0.0.32 +python-socketio==5.7.2 +pytz==2026.2 +pyyaml==6.0.3 +referencing==0.37.0 +regex==2026.7.19 +requests==2.34.2 +rich==15.0.0 +rpds-py==0.30.0 +ruff==0.15.22 +safetensors==0.8.0 +scikit-image==0.24.0 +scipy==1.15.3 +semantic-version==2.10.0 +setuptools==83.0.0 +shellingham==1.5.4 +simple-websocket==1.1.0 +six==1.17.0 +starlette==0.32.0.post1 +sympy==1.14.0 +tifffile==2025.5.10 +timm==1.0.28 +tokenizers==0.21.4 +tomlkit==0.12.0 +torch==2.13.0 +torchvision==0.28.0 +tqdm==4.69.0 +transformers==4.48.3 +typer==0.27.0 +typer-config==1.4.0 +typing-extensions==4.16.0 +typing-inspection==0.4.2 +tzdata==2026.3 +urllib3==2.7.0 +uvicorn==0.51.0 +websockets==11.0.3 +win32-setctime==1.2.0 +wsproto==1.3.2 +yacs==0.1.8 +zipp==4.1.0 + diff --git a/deploy/runtime/mask-requirements.txt b/deploy/runtime/mask-requirements.txt new file mode 100644 index 0000000..8312c53 --- /dev/null +++ b/deploy/runtime/mask-requirements.txt @@ -0,0 +1,44 @@ +accelerate==1.14.0 +annotated-doc==0.0.4 +anyio==4.14.2 +certifi==2026.7.22 +click==8.4.2 +colorama==0.4.6 +filelock==3.29.0 +fsspec==2026.4.0 +h11==0.16.0 +hf-xet==1.5.2 +httpcore==1.0.9 +httpx==0.28.1 +huggingface-hub==1.26.0 +idna==3.18 +jinja2==3.1.6 +llvmlite==0.48.0 +markdown-it-py==4.2.0 +markupsafe==3.0.3 +mdurl==0.1.2 +mpmath==1.3.0 +networkx==3.6.1 +numba==0.66.0 +numpy==2.4.2 +opencv-python-headless==5.0.0.93 +packaging==26.2 +pillow==12.3.0 +psutil==7.2.2 +pygments==2.20.0 +pymatting==1.1.15 +pyyaml==6.0.3 +regex==2026.7.19 +rich==15.0.0 +safetensors==0.8.0 +scipy==1.18.0 +setuptools==78.1.0 +shellingham==1.5.4 +sympy==1.14.0 +tokenizers==0.22.2 +torch==2.13.0 +torchvision==0.28.0 +tqdm==4.69.0 +transformers==5.14.1 +typer==0.27.0 +typing-extensions==4.15.0 diff --git a/scripts/install-runtime.py b/scripts/install-runtime.py new file mode 100644 index 0000000..5517302 --- /dev/null +++ b/scripts/install-runtime.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +"""Install the PixelTops image-editing runtime for repository deployment. + +This explicit deploy helper is the only repository entry point that creates or +updates ``$CODEX_HOME/tools/masked-image-edit``. It stages replacement Python +environments before activation, downloads pinned model artifacts, and never +runs from regular skill usage. Runtime validation is a separate deploy step. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import pathlib +import shutil +import subprocess +import sys +import urllib.request +import uuid +from collections.abc import Mapping, Sequence + + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] +CONTRACT_PATH = ( + REPO_ROOT + / "skills" + / "pixeltops-image-editor" + / "references" + / "runtime-contract.json" +) +ENVIRONMENT_MARKER = ".pixeltops-runtime.json" +MARKER_SCHEMA = "pixeltops-runtime-environment.v1" +LOCK_NAME = ".install-runtime.lock" + + +class InstallError(RuntimeError): + """One compact deployment-installation failure.""" + + +def required_mapping(value: object, label: str) -> Mapping[str, object]: + """Return one contract object or fail with its precise field label.""" + + if not isinstance(value, Mapping): + raise InstallError(f"runtime contract {label} must be an object") + return value + + +def required_text(value: object, label: str) -> str: + """Return one non-empty contract string.""" + + if not isinstance(value, str) or not value: + raise InstallError(f"runtime contract {label} must be a non-empty string") + return value + + +def portable_relative(value: object, label: str) -> pathlib.Path: + """Resolve a portable relative contract path without permitting traversal.""" + + text = required_text(value, label).replace("\\", "/") + path = pathlib.PurePosixPath(text) + windows = pathlib.PureWindowsPath(text) + if path.is_absolute() or windows.is_absolute() or windows.drive or ".." in path.parts: + raise InstallError(f"runtime contract {label} must be portable and relative") + return pathlib.Path(*path.parts) + + +def load_contract() -> Mapping[str, object]: + """Load the single runtime identity, dependency, and model source of truth.""" + + try: + contract = json.loads(CONTRACT_PATH.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise InstallError(f"runtime contract is unreadable: {exc}") from exc + mapping = required_mapping(contract, "root") + if mapping.get("schema") != "pixeltops-image-runtime.v1": + raise InstallError("runtime contract schema is unsupported") + return mapping + + +RUNTIME_CONTRACT = load_contract() +RUNTIME_RELATIVE = portable_relative( + RUNTIME_CONTRACT.get("runtime_relative_path"), + "runtime_relative_path", +) + + +def environment_declarations() -> tuple[tuple[str, str, pathlib.Path], ...]: + """Return the two closed deployment environment declarations.""" + + environments = required_mapping( + RUNTIME_CONTRACT.get("environments"), + "environments", + ) + declarations: list[tuple[str, str, pathlib.Path]] = [] + for name in ("mask", "inpaint"): + declaration = required_mapping(environments.get(name), f"environments.{name}") + python_version = required_text( + declaration.get("python"), + f"environments.{name}.python", + ) + requirements = portable_relative( + declaration.get("requirements"), + f"environments.{name}.requirements", + ) + declarations.append((name, python_version, REPO_ROOT / requirements)) + return tuple(declarations) + + +def huggingface_declarations() -> tuple[tuple[str, str], ...]: + """Return exact model identities and immutable revisions.""" + + models = required_mapping( + RUNTIME_CONTRACT.get("huggingface_models"), + "huggingface_models", + ) + declarations: list[tuple[str, str]] = [] + for name in ("grounding_dino", "sam2"): + declaration = required_mapping( + models.get(name), + f"huggingface_models.{name}", + ) + declarations.append( + ( + required_text(declaration.get("id"), f"huggingface_models.{name}.id"), + required_text( + declaration.get("revision"), + f"huggingface_models.{name}.revision", + ), + ) + ) + return tuple(declarations) + + +def codex_home() -> pathlib.Path: + """Return the configured Codex home without embedding a user-local path.""" + + configured = os.environ.get("CODEX_HOME") + return ( + pathlib.Path(configured).expanduser() + if configured + else pathlib.Path.home() / ".codex" + ) + + +def runtime_root() -> pathlib.Path: + """Return the machine-local runtime root owned by this installer.""" + + return codex_home() / RUNTIME_RELATIVE + + +def environment_relative(name: str) -> pathlib.Path: + """Return one environment location from the runtime contract.""" + + environments = required_mapping( + RUNTIME_CONTRACT.get("environments"), + "environments", + ) + declaration = required_mapping(environments.get(name), f"environments.{name}") + return portable_relative( + declaration.get("relative_path"), + f"environments.{name}.relative_path", + ) + + +def environment_python(root: pathlib.Path, name: str) -> pathlib.Path: + """Return the Windows interpreter path for one isolated environment.""" + + return root / environment_relative(name) / "Scripts" / "python.exe" + + +def run_checked( + arguments: Sequence[str], *, environment: Mapping[str, str] | None = None +) -> str: + """Run one deterministic installation command and retain compact failure.""" + + completed = subprocess.run( + list(arguments), + capture_output=True, + text=True, + check=False, + env=dict(environment) if environment is not None else None, + ) + if completed.returncode != 0: + detail = (completed.stderr or completed.stdout or "command failed").strip() + raise InstallError(detail[-2000:]) + return completed.stdout.strip() + + +def requirements_digest(path: pathlib.Path) -> str: + """Hash one pinned dependency set for idempotent environment reuse.""" + + if not path.is_file(): + raise InstallError(f"missing requirements: {path}") + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def read_marker(path: pathlib.Path) -> Mapping[str, object] | None: + """Read an environment marker, treating malformed state as stale.""" + + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + return value if isinstance(value, Mapping) else None + + +def environment_is_current( + python_exe: pathlib.Path, + *, + python_version: str, + requirements_sha256: str, +) -> bool: + """Check only installer-owned state; runtime readiness is validated later.""" + + marker = read_marker(python_exe.parents[1] / ENVIRONMENT_MARKER) + return bool( + python_exe.is_file() + and marker + and marker.get("schema") == MARKER_SCHEMA + and marker.get("python") == python_version + and marker.get("requirements_sha256") == requirements_sha256 + ) + + +def write_marker( + environment_root: pathlib.Path, + *, + python_version: str, + requirements_sha256: str, +) -> None: + """Record the exact installer inputs after package synchronization succeeds.""" + + marker = environment_root / ENVIRONMENT_MARKER + marker.write_text( + json.dumps( + { + "schema": MARKER_SCHEMA, + "python": python_version, + "requirements_sha256": requirements_sha256, + }, + indent=2, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + newline="\n", + ) + + +def install_environment( + uv: str, + root: pathlib.Path, + name: str, + python_version: str, + requirements: pathlib.Path, +) -> None: + """Stage and atomically activate one pinned Python environment.""" + + digest = requirements_digest(requirements) + target = root / environment_relative(name) + python_exe = environment_python(root, name) + if environment_is_current( + python_exe, + python_version=python_version, + requirements_sha256=digest, + ): + return + + target.parent.mkdir(parents=True, exist_ok=True) + token = uuid.uuid4().hex + stage = target.with_name(f".{name}.stage-{token}") + backup = target.with_name(f".{name}.backup-{token}") + try: + run_checked( + [ + uv, + "venv", + str(stage), + "--python", + python_version, + "--managed-python", + "--no-config", + ] + ) + staged_python = stage / "Scripts" / "python.exe" + run_checked( + [ + uv, + "pip", + "install", + "--python", + str(staged_python), + "--requirements", + str(requirements), + "--exact", + "--strict", + "--torch-backend", + "cpu", + "--no-progress", + "--no-config", + ] + ) + write_marker( + stage, + python_version=python_version, + requirements_sha256=digest, + ) + + if target.exists(): + target.rename(backup) + try: + stage.rename(target) + except OSError: + if backup.exists() and not target.exists(): + backup.rename(target) + raise + if backup.exists(): + shutil.rmtree(backup) + finally: + if stage.exists(): + shutil.rmtree(stage) + + +def install_huggingface_models(root: pathlib.Path) -> None: + """Download exact model revisions through the pinned mask environment.""" + + python_exe = environment_python(root, "mask") + cache = root / portable_relative( + RUNTIME_CONTRACT.get("huggingface_cache"), + "huggingface_cache", + ) + cache.mkdir(parents=True, exist_ok=True) + code = ( + "from huggingface_hub import snapshot_download; import sys; " + "snapshot_download(repo_id=sys.argv[1], revision=sys.argv[2], " + "cache_dir=sys.argv[3])" + ) + for model_id, revision in huggingface_declarations(): + run_checked([str(python_exe), "-c", code, model_id, revision, str(cache)]) + + +def file_md5(path: pathlib.Path) -> str: + """Return the upstream integrity digest used by IOPaint for LaMa.""" + + digest = hashlib.md5(usedforsecurity=False) + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def install_lama_model(root: pathlib.Path) -> None: + """Download LaMa to a temporary peer and activate only after verification.""" + + lama = required_mapping(RUNTIME_CONTRACT.get("lama"), "lama") + target = root / portable_relative(lama.get("relative_path"), "lama.relative_path") + expected_md5 = required_text(lama.get("md5"), "lama.md5") + source_url = required_text(lama.get("url"), "lama.url") + if target.is_file() and file_md5(target) == expected_md5: + return + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp") + request = urllib.request.Request( + source_url, + headers={"User-Agent": "PixelTops-Runtime-Installer/1"}, + ) + try: + with urllib.request.urlopen(request, timeout=120) as response, temporary.open( + "wb" + ) as output: + shutil.copyfileobj(response, output, length=1024 * 1024) + if file_md5(temporary) != expected_md5: + raise InstallError("downloaded LaMa model failed integrity validation") + os.replace(temporary, target) + finally: + temporary.unlink(missing_ok=True) + + +def acquire_lock(root: pathlib.Path) -> pathlib.Path: + """Block concurrent installers without touching unrelated runtime state.""" + + root.mkdir(parents=True, exist_ok=True) + lock = root / LOCK_NAME + try: + descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except FileExistsError as exc: + raise InstallError(f"runtime installation is already locked: {lock}") from exc + with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle: + handle.write(f"{os.getpid()}\n") + return lock + + +def main() -> int: + """Install declared runtime inputs and emit only the compact result.""" + + try: + if sys.argv[1:]: + raise InstallError("install-runtime accepts no arguments") + if os.name != "nt": + raise InstallError("the current image-editing runtime supports Windows only") + uv = shutil.which("uv") + if uv is None: + raise InstallError("uv is required on PATH for install-runtime") + root = runtime_root() + lock = acquire_lock(root) + try: + for name, python_version, requirements in environment_declarations(): + install_environment( + uv, + root, + name, + python_version, + requirements, + ) + install_huggingface_models(root) + install_lama_model(root) + finally: + lock.unlink(missing_ok=True) + except (InstallError, OSError, subprocess.SubprocessError) as exc: + print( + json.dumps( + {"status": "ERROR", "error": str(exc)}, + separators=(",", ":"), + ) + ) + return 1 + print("OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/install-skills-bootstrap.py b/scripts/install-skills-bootstrap.py new file mode 100644 index 0000000..0e0c2e0 --- /dev/null +++ b/scripts/install-skills-bootstrap.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python3 +"""Bootstrap this repository's declared skills without lifecycle dependencies. + +This first-install-only helper stages one complete selected batch in a uniquely +named hidden directory under the install root. It validates that batch before +activation, never replaces an existing skill, and cleans only staging and lock +paths that it created. +""" + +from __future__ import annotations + +import argparse +import json +import os +import pathlib +import re +import shutil +import stat +import sys +import uuid +from collections.abc import Mapping, Sequence +from typing import cast + + +INSTALLER_VERSION = 10 +MANIFEST_NAME = ".runtime-manifest.json" +RUNTIME_MANIFEST_SCHEMA = "ceratops-runtime-skill.v3" +START = "" +END = "" +SOURCE_PREFIX = "" +LOCK_NAME = ".ceratops-bootstrap.lock" +STAGE_RE = re.compile(r"^\.ceratops-bootstrap-stage-[0-9a-f]{32}$") +SKILL_NAME_RE = re.compile( + r"^(?![a-z0-9-]*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$" +) +IGNORED_NAMES = { + ".git", + "__pycache__", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "node_modules", +} + + +def fail(message: str) -> int: + """Emit one concise fatal error.""" + + print(message, file=sys.stderr) + return 1 + + +def safe_relative(value: str) -> bool: + """Accept only repository-relative manifest paths and patterns.""" + + posix = pathlib.PurePosixPath(value.replace("\\", "/")) + windows = pathlib.PureWindowsPath(value) + return bool( + value + and not posix.is_absolute() + and not windows.is_absolute() + and not windows.drive + and ".." not in posix.parts + ) + + +def unsafe_link(path: pathlib.Path) -> bool: + """Reject links and Windows reparse points from copied input.""" + + if path.is_symlink(): + return True + if os.name != "nt": + return False + attributes = getattr( + path.stat(follow_symlinks=False), "st_file_attributes", 0 + ) + return bool( + attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400) + ) + + +def require_inside(path: pathlib.Path, root: pathlib.Path) -> None: + """Reject any resolved path that escapes its declared root.""" + + path.resolve(strict=False).relative_to(root.resolve()) + + +def validate_tree(root: pathlib.Path) -> None: + """Reject links or reparse points anywhere in one staged tree.""" + + if unsafe_link(root): + raise ValueError(f"unsafe staged tree root: {root}") + for path in root.rglob("*"): + if unsafe_link(path): + raise ValueError(f"unsafe staged tree entry: {path}") + + +def read_manifest(repo_root: pathlib.Path) -> dict[str, object]: + """Read and validate the declarations required to render every skill.""" + + path = repo_root / "skills" / "skill-sections.json" + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError("skill-sections.json must contain an object") + source_id = value.get("runtime_source_id") + profile = value.get("validation_profile", "ceratops-compatible") + sections = value.get("sections") + skills = value.get("skills") + payloads = value.get("runtime_payloads", {}) + if not isinstance(source_id, str) or not source_id.strip(): + raise ValueError("runtime_source_id must be a nonempty string") + if profile not in {"ceratops", "ceratops-compatible"}: + raise ValueError("validation_profile is unsupported") + if not isinstance(sections, dict) or not all( + isinstance(name, str) and isinstance(relative, str) + for name, relative in sections.items() + ): + raise ValueError("sections must map strings to strings") + if not isinstance(skills, dict) or not all( + isinstance(name, str) and isinstance(selected, list) + for name, selected in skills.items() + ): + raise ValueError("skills must map names to section lists") + if not isinstance(payloads, dict): + raise ValueError("runtime_payloads must be an object") + source_names = { + skill.parent.name + for skill in (repo_root / "skills").glob("*/SKILL.md") + } + if set(skills) != source_names: + raise ValueError("skill assignments must match source SKILL.md folders") + return value + + +def declared_skills( + manifest: Mapping[str, object], requested: Sequence[str] +) -> list[str]: + """Resolve the exact declared skill set before staging output.""" + + assignments = cast(Mapping[str, object], manifest["skills"]) + names = list(requested) if requested else sorted(assignments) + if len(names) != len(set(names)): + raise ValueError("duplicate --skill selection") + for name in names: + if not isinstance(name, str) or not SKILL_NAME_RE.fullmatch(name): + raise ValueError(f"invalid skill name: {name!r}") + if name not in assignments: + raise ValueError(f"undeclared skill: {name}") + return names + + +def section_block( + repo_root: pathlib.Path, + manifest: Mapping[str, object], + skill: str, +) -> str: + """Resolve one skill's shared sections without lifecycle runtime code.""" + + sections = cast(Mapping[str, object], manifest["sections"]) + assignments = cast(Mapping[str, object], manifest["skills"]) + selected = assignments[skill] + if not isinstance(selected, list) or not selected: + raise ValueError(f"{skill}: section assignment must be a nonempty list") + rendered: list[str] = [] + for name in selected: + if not isinstance(name, str) or name not in sections: + raise ValueError(f"{skill}: unresolved section {name!r}") + relative = sections[name] + if not isinstance(relative, str) or not safe_relative(relative): + raise ValueError(f"{skill}: invalid section path {relative!r}") + path = repo_root / relative + require_inside(path, repo_root) + if not path.is_file() or unsafe_link(path): + raise ValueError(f"{skill}: unavailable section {relative}") + lines = path.read_text(encoding="utf-8").splitlines() + text = "\n".join( + line + for line in lines + if not line.strip().startswith(" + +## Instruction enforcement + +- All instruction bullets in this file are mandatory, blocking, and + closure-gating for the phase, action, decision, artifact, or response they + govern. +- Do not proceed with or claim completion for any action, decision, artifact, or + response when an applicable instruction bullet is unmet, unverifiable, or in + conflict; report the blocker or conflict instead. +- The `(D)` label marks a deterministic script or executable-helper contract; it + does not change the mandatory status of labeled or unlabeled rules. + +## Core Rules + +- When this skill is invoked, follow this `SKILL.md` as the workflow contract + for the task; if a higher-precedence instruction conflicts with a required + skill step, report the conflict instead of silently skipping the step. +- Do not claim completion unless this skill's completion gate is satisfied, + intentionally inapplicable, or reported as a blocker. +- Scope completion, current-state, root-cause, no-fix, unsupported, and + durable-resolution claims to evidence actually checked, or to fresh same-task + evidence that still applies. +- Reuse fresh sufficient same-run evidence unless state is uncertain, plausibly + changed, materially broadened, externally mutable for the decision, or this + skill explicitly requires a fresh check. +- Prefer direct local evidence and targeted diagnostics for the next skill + decision; use current official sources only when local evidence leaves a + concrete ambiguity or the task depends on unstable external behavior. +- When two active sources use different identities for the same required item, + treat the producer/current source as authoritative, update stale consumers or + gates to match it, and ask if authority is unclear. +- Do not do generalized best-practice refresh, reference-repo comparison, or + skill-maintenance work during routine skill runs unless the user explicitly + asks or a required decision remains ambiguous after targeted evidence. +- Ask before risky, destructive, irreversible, credential-dependent, externally + mutating, complex, invasive, nonstandard, or high-maintenance steps unless the + user already explicitly requested that tradeoff. +- Do not update this `SKILL.md` or other skill/control files during a routine + run unless the user explicitly asked for skill maintenance or the task cannot + be completed safely without a narrow in-scope fix. +- For skill runtime workflows, invoke shared helpers through installed console + commands, `python -m ` entrypoints, or scripts copied into the + installed skill folder; do not locate shared helpers by absolute paths or by + the repo's parent directory. +- When a workflow needs a shared repo-maintenance script, run `scripts/` + from the active source checkout root when available, otherwise from the + installed skill folder; when a helper is skill-local, run it from that skill + folder or the corresponding source skill folder; stop as blocked if neither + declared location contains it. +- When editing an existing text file, preserve its current line-ending + convention unless intentional normalization is part of the task. +- Follow this skill's output contract when present; otherwise report only the + outcome, unresolved blockers, retained state with reasons, and important + unverified items. + +## Credential Handling + +- Do not ask for credentials unless they are truly required after local checks. +- If credentials are truly required after local checks, report only: + +1. which credential or login is missing +2. why it is needed +3. where it will be stored +4. the exact command the user should run +5. whether it goes into a local credential store, config file, keyring, CI + secret, registry setting, connector, or another exact target + +- If the user refuses a missing permission, credential, login, or scope, stop + retrying and report the blocked action and exact entities still pending. diff --git a/templates/skill-sections.json b/skills/skill-sections.json similarity index 74% rename from templates/skill-sections.json rename to skills/skill-sections.json index 5c30470..16f5653 100644 --- a/templates/skill-sections.json +++ b/skills/skill-sections.json @@ -2,13 +2,13 @@ "runtime_source_id": "pixeltops", "validation_profile": "ceratops-compatible", "sections": { - "core": "templates/sections/core.md" + "core": "skills/sections/core.md" }, + "maintenance_workflows": {}, + "runtime_payloads": {}, "skills": { "pixeltops-image-editor": [ "core" ] - }, - "maintenance_workflows": {}, - "runtime_payloads": {} + } } diff --git a/templates/sections/core.md b/templates/sections/core.md deleted file mode 100644 index 8aac3c9..0000000 --- a/templates/sections/core.md +++ /dev/null @@ -1,19 +0,0 @@ -## Instruction enforcement - -- Treat every instruction bullet in this skill as mandatory and closure-gating - for the action it governs. -- Do not claim completion when required evidence is missing; report the exact - blocker or retained uncertainty. - -## Core rules - -- Use the skill-local executable helpers for deterministic image operations - and treat their JSON results as the operation record. -- Preserve the user's source image by default and require explicit authority - before overwriting an existing artifact. -- Match every untouched-pixel claim to an explicit allowed-change mask and a - successful byte-level verification result. -- Ask before widening the requested image, mask, output, or external-service - scope; ambiguous object selection alone does not authorize a guess. -- Report only output artifacts, the effective selection and allowed masks, - verification results, blockers, and important retained variability. diff --git a/tests/test_validate_repository.py b/tests/test_validate_repository.py index 5bc16b2..b14f27e 100644 --- a/tests/test_validate_repository.py +++ b/tests/test_validate_repository.py @@ -61,7 +61,13 @@ def test_orchestrates_all_checks_in_order_and_emits_only_ok(self) -> None: sys.executable, "-m", "json.tool", - "templates/skill-sections.json", + "skills/skill-sections.json", + ), + ( + sys.executable, + "-m", + "json.tool", + "skills/pixeltops-image-editor/references/runtime-contract.json", ), ( sys.executable, @@ -95,14 +101,24 @@ def test_orchestrates_all_checks_in_order_and_emits_only_ok(self) -> None: ] self.assertEqual( [record["check"] for record in evidence], - ["section-manifest", "python-compilation", "mypy", "unit-tests"], + [ + "section-manifest", + "runtime-contract", + "python-compilation", + "mypy", + "unit-tests", + ], ) self.assertTrue(all(record["status"] == "OK" for record in evidence)) def test_propagates_failure_and_stops_with_compact_json(self) -> None: with tempfile.TemporaryDirectory() as directory: evidence_file = pathlib.Path(directory) / "validation.jsonl" - results = [completed(), completed(7, "detail out", "detail err")] + results = [ + completed(), + completed(), + completed(7, "detail out", "detail err"), + ] exit_code, stdout, stderr, run = self.invoke( evidence_file, REPO_ROOT, results @@ -120,7 +136,7 @@ def test_propagates_failure_and_stops_with_compact_json(self) -> None: json.dumps(expected, separators=(",", ":"), sort_keys=True) + "\n", ) self.assertEqual(stderr, "") - self.assertEqual(run.call_count, 2) + self.assertEqual(run.call_count, 3) evidence = [ json.loads(line) for line in evidence_file.read_text(encoding="utf-8").splitlines() @@ -131,13 +147,20 @@ def test_propagates_failure_and_stops_with_compact_json(self) -> None: def test_handles_repository_and_evidence_paths_with_spaces(self) -> None: with tempfile.TemporaryDirectory() as directory: repo_root = pathlib.Path(directory) / "repository with spaces" - (repo_root / "templates").mkdir(parents=True) + (repo_root / "skills").mkdir(parents=True) (repo_root / "scripts").mkdir() (repo_root / "skills" / "pixeltops-image-editor" / "scripts").mkdir( parents=True ) (repo_root / "tests").mkdir() - (repo_root / "templates" / "skill-sections.json").write_text( + (repo_root / "skills" / "skill-sections.json").write_text( + "{}\n", encoding="utf-8" + ) + runtime_references = ( + repo_root / "skills" / "pixeltops-image-editor" / "references" + ) + runtime_references.mkdir(parents=True) + (runtime_references / "runtime-contract.json").write_text( "{}\n", encoding="utf-8" ) (repo_root / "scripts" / "example.py").write_text( From 3bcb6a6e1b1d5cab696dd80aa3f6e03a23c2dc65 Mon Sep 17 00:00:00 2001 From: RomanOstr <46819322+RomanOstr@users.noreply.github.com> Date: Mon, 3 Aug 2026 04:00:36 +0300 Subject: [PATCH 2/2] Fix inpainting lockfile EOF --- deploy/runtime/inpaint-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy/runtime/inpaint-requirements.txt b/deploy/runtime/inpaint-requirements.txt index f554623..29493ff 100644 --- a/deploy/runtime/inpaint-requirements.txt +++ b/deploy/runtime/inpaint-requirements.txt @@ -104,4 +104,3 @@ win32-setctime==1.2.0 wsproto==1.3.2 yacs==0.1.8 zipp==4.1.0 -