Files
admin 24e3deb34e
Check instruction packages / check (pull_request) Successful in 3s
Check instruction packages / check (push) Successful in 3s
Let replacement Planner packages declare issue claim policy
2026-10-12 09:49:31 -04:00

180 lines
8.3 KiB
Python

"""Dependency-free checks and reproducible archives for instruction packages."""
import argparse
import hashlib
import json
import re
import stat
import struct
import zipfile
from pathlib import Path
class PackageError(ValueError):
pass
def _fail(message):
raise PackageError(message)
def _text(data, label):
try:
value = data.decode("utf-8")
except UnicodeDecodeError:
_fail(f"{label}: invalid UTF-8")
if not value or value.startswith("\ufeff") or "\x00" in value:
_fail(f"{label}: invalid text")
return value
def _frame(value):
return struct.pack(">I", len(value)) + value
def check_package(path):
path = Path(path)
if path.is_symlink() or not path.is_dir():
_fail(f"{path}: package must be a directory")
sidecar = path / "relayview-skill.json"
if sidecar.is_symlink() or not sidecar.is_file() or sidecar.stat().st_size > 65536:
_fail(f"{path}: missing or invalid sidecar")
raw_manifest = sidecar.read_bytes()
try:
manifest = json.loads(_text(raw_manifest, str(sidecar)))
except (ValueError, TypeError) as error:
_fail(f"{sidecar}: invalid JSON: {error}")
if not isinstance(manifest, dict) or (
(manifest.get("schemaVersion") == 1 and set(manifest) != {"schemaVersion", "source", "role", "step", "files"}) or
(manifest.get("schemaVersion") == 2 and set(manifest) != {"schemaVersion", "source", "role", "step", "files", "issueClaim"}) or
manifest.get("schemaVersion") not in (1, 2)
):
_fail(f"{path}: unsupported manifest fields")
source = manifest["source"]
if not isinstance(source, dict) or set(source) != {"kind", "reference"}:
_fail(f"{path}: invalid source")
if source["kind"] not in ("first-party", "user") or not isinstance(source["reference"], str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", source["reference"]):
_fail(f"{path}: invalid source identity")
if (manifest["role"], manifest["step"]) not in (("planner", "planning"), ("implementer", "implementation"), ("reviewer", "review")):
_fail(f"{path}: incompatible role or step")
if manifest["schemaVersion"] == 2:
claim = manifest["issueClaim"]
label = claim.get("statusLabel") if isinstance(claim, dict) else None
if (manifest["role"], manifest["step"]) != ("planner", "planning") or not isinstance(claim, dict) or set(claim) != {"statusLabel"} or (
label is not None and (not isinstance(label, str) or not re.fullmatch(r"status/[a-z][a-z0-9-]{0,63}", label) or label == "status/ready-for-agent")
):
_fail(f"{path}: invalid issue claim policy")
files = manifest["files"]
if not isinstance(files, list) or not 1 <= len(files) <= 16:
_fail(f"{path}: invalid file list")
declared = {}
for item in files:
if not isinstance(item, dict) or set(item) != {"path", "sha256"}:
_fail(f"{path}: invalid file entry")
name, digest = item["path"], item["sha256"]
if not isinstance(name, str) or len(name) > 256 or "\\" in name or any(part in ("", ".", "..") for part in name.split("/")) or name.startswith("/"):
_fail(f"{path}: invalid file path")
if name != "SKILL.md" and not name.endswith((".md", ".txt")):
_fail(f"{path}: unsupported package content")
if name in declared or not isinstance(digest, str) or not re.fullmatch(r"[a-f0-9]{64}", digest):
_fail(f"{path}: duplicate file or invalid digest")
declared[name] = digest
if "SKILL.md" not in declared:
_fail(f"{path}: SKILL.md is required")
observed = {}
total = len(raw_manifest)
entry_count = 0
for file in path.rglob("*"):
entry_count += 1
relative = file.relative_to(path).as_posix()
if entry_count > 64 or len(file.relative_to(path).parts) > 8 or file.is_symlink():
_fail(f"{path}: package entry exceeds bounds or is a link")
if file.is_dir():
continue
if not file.is_file() or file.stat().st_mode & 0o111:
_fail(f"{path}: executable or unsupported file")
if relative == "relayview-skill.json":
continue
if relative not in declared:
_fail(f"{path}: undeclared file {relative}")
raw = file.read_bytes()
total += len(raw)
if not 1 <= len(raw) <= 1000000 or total > 131072:
_fail(f"{path}: package exceeds text bounds")
_text(raw, relative)
if hashlib.sha256(raw).hexdigest() != declared[relative]:
_fail(f"{path}: changed file {relative}")
observed[relative] = raw
if set(observed) != set(declared):
_fail(f"{path}: missing declared file")
skill = _text(observed["SKILL.md"], "SKILL.md")
match = re.match(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|\Z)", skill, re.S)
if not match:
_fail(f"{path}: invalid SKILL.md frontmatter")
frontmatter = match.group(1)
top_keys = []
nested_keys = []
in_metadata = False
for line in frontmatter.splitlines():
if line.startswith(" "):
if not in_metadata or not re.fullmatch(r" [a-z-]+: .+", line):
_fail(f"{path}: unsupported skill metadata")
nested_keys.append(line[2:].split(":", 1)[0])
elif re.match(r"^[a-z-]+:", line):
key = line.split(":", 1)[0]
top_keys.append(key)
in_metadata = key == "metadata"
else:
_fail(f"{path}: unsupported skill metadata")
if len(top_keys) != len(set(top_keys)) or set(top_keys) - {"name", "description", "metadata", "disable-model-invocation"} or len(nested_keys) != len(set(nested_keys)) or set(nested_keys) != {"relayview-display-name", "relayview-version"}:
_fail(f"{path}: unsupported skill metadata")
values = dict(line.split(": ", 1) for line in frontmatter.splitlines() if not line.startswith(" ") and ": " in line)
if "disable-model-invocation" in values and values["disable-model-invocation"] not in ("true", "false"):
_fail(f"{path}: disable-model-invocation must be a boolean")
expected_name = path.name
if not re.search(rf"(?m)^name: {re.escape(expected_name)}$", frontmatter) or not re.search(r"(?m)^description: .+$", frontmatter) or not re.search(r"(?m)^ relayview-display-name: .+$", frontmatter) or not re.search(r"(?m)^ relayview-version: [0-9]+\.[0-9]+\.[0-9]+$", frontmatter):
_fail(f"{path}: missing or incompatible skill metadata")
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", expected_name) or len(expected_name) > 64 or len(values["description"]) > 1024:
_fail(f"{path}: skill name or description exceeds owner bounds")
display_name = next(line.split(": ", 1)[1] for line in frontmatter.splitlines() if line.startswith(" relayview-display-name: "))
if len(display_name) > 128:
_fail(f"{path}: display name exceeds owner bounds")
digest = hashlib.sha256(b"relayview-instruction-package-v1\0" + _frame(raw_manifest))
for name in sorted(declared):
digest.update(_frame(name.encode("utf-8")))
digest.update(_frame(observed[name]))
return digest.hexdigest()
def package_archive(path, destination):
path, destination = Path(path), Path(destination)
check_package(path)
destination.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(destination, "w", compression=zipfile.ZIP_STORED) as archive:
root = Path(__file__).resolve().parents[1]
files = [(name, root / name) for name in ("LICENSE", "NOTICE.md")]
files += [(f"{path.name}/{file.relative_to(path).as_posix()}", file) for file in sorted(path.rglob("*")) if file.is_file()]
for name, file in files:
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
info.external_attr = 0o100644 << 16
archive.writestr(info, file.read_bytes())
return hashlib.sha256(destination.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=("check", "pack"))
parser.add_argument("path", type=Path)
parser.add_argument("destination", type=Path, nargs="?")
args = parser.parse_args()
if args.action == "pack":
if args.destination is None:
parser.error("pack requires a destination")
print(package_archive(args.path, args.destination))
else:
print(check_package(args.path))
if __name__ == "__main__":
main()